rigortype 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +1 -0
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -5,6 +5,9 @@ require "prism"
|
|
|
5
5
|
require_relative "../scope"
|
|
6
6
|
require_relative "../type"
|
|
7
7
|
require_relative "../source/constant_path"
|
|
8
|
+
require_relative "../source/node_children"
|
|
9
|
+
require_relative "../cache/file_digest"
|
|
10
|
+
require_relative "def_handle"
|
|
8
11
|
require_relative "mutation_widening"
|
|
9
12
|
require_relative "narrowing"
|
|
10
13
|
require_relative "statement_evaluator"
|
|
@@ -310,7 +313,7 @@ module Rigor
|
|
|
310
313
|
end
|
|
311
314
|
end
|
|
312
315
|
|
|
313
|
-
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator, mutated_ivars, # rubocop:disable Metrics/
|
|
316
|
+
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator, mutated_ivars, # rubocop:disable Metrics/ParameterLists
|
|
314
317
|
read_before_write = nil, init_writes = nil, method_assign_effects = nil)
|
|
315
318
|
return unless node.is_a?(Prism::Node)
|
|
316
319
|
|
|
@@ -346,7 +349,7 @@ module Rigor
|
|
|
346
349
|
end
|
|
347
350
|
end
|
|
348
351
|
|
|
349
|
-
node.
|
|
352
|
+
node.rigor_each_child do |child|
|
|
350
353
|
walk_class_ivars(child, qualified_prefix, default_scope, accumulator,
|
|
351
354
|
mutated_ivars, read_before_write, init_writes, method_assign_effects)
|
|
352
355
|
end
|
|
@@ -511,7 +514,7 @@ module Rigor
|
|
|
511
514
|
(init_writes[class_name] ||= Set.new) << node.name
|
|
512
515
|
end
|
|
513
516
|
|
|
514
|
-
node.
|
|
517
|
+
node.rigor_each_child do |child|
|
|
515
518
|
collect_class_body_ivar_writes(child, class_name, init_writes)
|
|
516
519
|
end
|
|
517
520
|
end
|
|
@@ -522,16 +525,49 @@ module Rigor
|
|
|
522
525
|
|
|
523
526
|
read_first << node.name if node.is_a?(Prism::InstanceVariableReadNode) && !seen_writes.include?(node.name)
|
|
524
527
|
|
|
528
|
+
# N1 — parallel / multiple assignment (`@m, @n = [], []`). The ivar targets are `InstanceVariableTargetNode`s,
|
|
529
|
+
# not `InstanceVariableWriteNode`s, so the generic descent below never records them as writes. Left unhandled,
|
|
530
|
+
# an ivar written only via massign in `initialize` stays absent from `init_writes`, and
|
|
531
|
+
# `contribute_read_before_write_nil!` then unions a spurious `nil` into its class-ivar seed — masking a
|
|
532
|
+
# genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any target is
|
|
533
|
+
# committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every ivar
|
|
534
|
+
# target as written.
|
|
535
|
+
if node.is_a?(Prism::MultiWriteNode)
|
|
536
|
+
detect_read_before_write(node.value, seen_writes, read_first) if node.value
|
|
537
|
+
detect_multi_write_target_writes(node, seen_writes, read_first)
|
|
538
|
+
return
|
|
539
|
+
end
|
|
540
|
+
|
|
525
541
|
# Descend BEFORE recording a write — `@x = @x + 1`'s RHS is an `InstanceVariableReadNode` that runs before the
|
|
526
|
-
# write is committed; the read is therefore read-before-write semantically.
|
|
527
|
-
#
|
|
528
|
-
node.
|
|
542
|
+
# write is committed; the read is therefore read-before-write semantically. `each_child` yields the value
|
|
543
|
+
# child before the lvalue target (`compact_child_nodes` field order), matching this order.
|
|
544
|
+
node.rigor_each_child do |c|
|
|
529
545
|
detect_read_before_write(c, seen_writes, read_first)
|
|
530
546
|
end
|
|
531
547
|
|
|
532
548
|
seen_writes << node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
|
|
533
549
|
end
|
|
534
550
|
|
|
551
|
+
# Records each ivar target of a `MultiWriteNode` / nested `MultiTargetNode` into `seen_writes`, and descends into
|
|
552
|
+
# any non-ivar target (a `CallTargetNode` / `IndexTargetNode` receiver such as `@obj.x, @y = …`) so an ivar read
|
|
553
|
+
# inside a target receiver still counts as read-before-write.
|
|
554
|
+
def detect_multi_write_target_writes(node, seen_writes, read_first)
|
|
555
|
+
targets = (node.lefts || []) + [node.rest].compact + (node.rights || [])
|
|
556
|
+
targets.each do |target|
|
|
557
|
+
case target
|
|
558
|
+
when Prism::InstanceVariableTargetNode
|
|
559
|
+
seen_writes << target.name
|
|
560
|
+
when Prism::MultiTargetNode
|
|
561
|
+
detect_multi_write_target_writes(target, seen_writes, read_first)
|
|
562
|
+
when Prism::SplatNode
|
|
563
|
+
inner = target.expression
|
|
564
|
+
seen_writes << inner.name if inner.is_a?(Prism::InstanceVariableTargetNode)
|
|
565
|
+
else
|
|
566
|
+
detect_read_before_write(target, seen_writes, read_first)
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
535
571
|
IVAR_BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
|
|
536
572
|
private_constant :IVAR_BARRIER_NODES
|
|
537
573
|
|
|
@@ -568,7 +604,7 @@ module Rigor
|
|
|
568
604
|
return
|
|
569
605
|
end
|
|
570
606
|
|
|
571
|
-
node.
|
|
607
|
+
node.rigor_each_child do |c|
|
|
572
608
|
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes)
|
|
573
609
|
end
|
|
574
610
|
end
|
|
@@ -742,7 +778,7 @@ module Rigor
|
|
|
742
778
|
return acc
|
|
743
779
|
end
|
|
744
780
|
|
|
745
|
-
root.
|
|
781
|
+
root.rigor_each_child { |c| collect_class_method_defs(c, prefix, acc) }
|
|
746
782
|
acc
|
|
747
783
|
end
|
|
748
784
|
|
|
@@ -773,7 +809,7 @@ module Rigor
|
|
|
773
809
|
return acc unless node.is_a?(Prism::Node)
|
|
774
810
|
|
|
775
811
|
acc << node.name if node.is_a?(Prism::InstanceVariableWriteNode) && !nil_literal_value?(node.value)
|
|
776
|
-
node.
|
|
812
|
+
node.rigor_each_child { |c| ivar_write_targets(c, acc) }
|
|
777
813
|
acc
|
|
778
814
|
end
|
|
779
815
|
|
|
@@ -1106,7 +1142,7 @@ module Rigor
|
|
|
1106
1142
|
return
|
|
1107
1143
|
end
|
|
1108
1144
|
|
|
1109
|
-
node.
|
|
1145
|
+
node.rigor_each_child do |child|
|
|
1110
1146
|
walk_class_cvars(child, qualified_prefix, default_scope, accumulator)
|
|
1111
1147
|
end
|
|
1112
1148
|
end
|
|
@@ -1125,7 +1161,7 @@ module Rigor
|
|
|
1125
1161
|
record_cvar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::ClassVariableWriteNode)
|
|
1126
1162
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1127
1163
|
|
|
1128
|
-
node.
|
|
1164
|
+
node.rigor_each_child { |c| gather_cvar_writes(c, scope, class_name, accumulator) }
|
|
1129
1165
|
end
|
|
1130
1166
|
|
|
1131
1167
|
def record_cvar_write(node, scope, class_name, accumulator)
|
|
@@ -1149,7 +1185,7 @@ module Rigor
|
|
|
1149
1185
|
return unless node.is_a?(Prism::Node)
|
|
1150
1186
|
|
|
1151
1187
|
record_global_write(node, scope, accumulator) if node.is_a?(Prism::GlobalVariableWriteNode)
|
|
1152
|
-
node.
|
|
1188
|
+
node.rigor_each_child { |c| gather_global_writes(c, scope, accumulator) }
|
|
1153
1189
|
end
|
|
1154
1190
|
|
|
1155
1191
|
def record_global_write(node, scope, accumulator)
|
|
@@ -1189,7 +1225,7 @@ module Rigor
|
|
|
1189
1225
|
return
|
|
1190
1226
|
end
|
|
1191
1227
|
|
|
1192
|
-
node.
|
|
1228
|
+
node.rigor_each_child do |child|
|
|
1193
1229
|
walk_constant_writes(child, qualified_prefix, default_scope, accumulator)
|
|
1194
1230
|
end
|
|
1195
1231
|
end
|
|
@@ -1249,7 +1285,7 @@ module Rigor
|
|
|
1249
1285
|
end
|
|
1250
1286
|
end
|
|
1251
1287
|
|
|
1252
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1288
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1253
1289
|
# Combined `walk_methods` + `walk_def_nodes` descent. The two walks had identical class / module / singleton-class
|
|
1254
1290
|
# / meta-block traversals and both stopped at `DefNode`; the only divergences are leaf actions (recorded into the
|
|
1255
1291
|
# right accumulator) and the original `walk_methods` returning at `AliasMethodNode` (its symbol-only children
|
|
@@ -1295,7 +1331,7 @@ module Rigor
|
|
|
1295
1331
|
end
|
|
1296
1332
|
end
|
|
1297
1333
|
|
|
1298
|
-
node.
|
|
1334
|
+
node.rigor_each_child do |child|
|
|
1299
1335
|
walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
|
|
1300
1336
|
end
|
|
1301
1337
|
end
|
|
@@ -1323,7 +1359,7 @@ module Rigor
|
|
|
1323
1359
|
end
|
|
1324
1360
|
end
|
|
1325
1361
|
end
|
|
1326
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1362
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1327
1363
|
|
|
1328
1364
|
# v0.1.2 — when a `Const = Data.define(*sym) do ... end` / `Const = Struct.new(*sym) do ... end` constant write
|
|
1329
1365
|
# carries a block, the block body holds method overrides whose canonical class is `Const`. Survey item (e)
|
|
@@ -1474,7 +1510,7 @@ module Rigor
|
|
|
1474
1510
|
return
|
|
1475
1511
|
end
|
|
1476
1512
|
|
|
1477
|
-
node.
|
|
1513
|
+
node.rigor_each_child do |child|
|
|
1478
1514
|
walk_singleton_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
|
|
1479
1515
|
end
|
|
1480
1516
|
end
|
|
@@ -1586,7 +1622,7 @@ module Rigor
|
|
|
1586
1622
|
end
|
|
1587
1623
|
end
|
|
1588
1624
|
|
|
1589
|
-
node.
|
|
1625
|
+
node.rigor_each_child do |child|
|
|
1590
1626
|
walk_class_superclasses(child, qualified_prefix, accumulator)
|
|
1591
1627
|
end
|
|
1592
1628
|
end
|
|
@@ -1623,7 +1659,7 @@ module Rigor
|
|
|
1623
1659
|
record_data_member_layout(accumulator, qualified_prefix + [node.name.to_s], node.value)
|
|
1624
1660
|
end
|
|
1625
1661
|
|
|
1626
|
-
node.
|
|
1662
|
+
node.rigor_each_child do |child|
|
|
1627
1663
|
walk_data_member_layouts(child, qualified_prefix, accumulator)
|
|
1628
1664
|
end
|
|
1629
1665
|
end
|
|
@@ -1670,7 +1706,7 @@ module Rigor
|
|
|
1670
1706
|
record_struct_member_layout(accumulator, qualified_prefix + [node.name.to_s], node.value)
|
|
1671
1707
|
end
|
|
1672
1708
|
|
|
1673
|
-
node.
|
|
1709
|
+
node.rigor_each_child do |child|
|
|
1674
1710
|
walk_struct_member_layouts(child, qualified_prefix, accumulator)
|
|
1675
1711
|
end
|
|
1676
1712
|
end
|
|
@@ -1731,7 +1767,7 @@ module Rigor
|
|
|
1731
1767
|
record_mixin_call(node, current_class, accumulator)
|
|
1732
1768
|
end
|
|
1733
1769
|
|
|
1734
|
-
node.
|
|
1770
|
+
node.rigor_each_child do |child|
|
|
1735
1771
|
walk_class_includes(child, qualified_prefix, current_class, accumulator)
|
|
1736
1772
|
end
|
|
1737
1773
|
end
|
|
@@ -1772,7 +1808,7 @@ module Rigor
|
|
|
1772
1808
|
accumulator.transform_values(&:freeze).freeze
|
|
1773
1809
|
end
|
|
1774
1810
|
|
|
1775
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/
|
|
1811
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1776
1812
|
def walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
|
|
1777
1813
|
return current_visibility unless node.is_a?(Prism::Node)
|
|
1778
1814
|
|
|
@@ -1810,18 +1846,18 @@ module Rigor
|
|
|
1810
1846
|
# entry visibility unchanged.
|
|
1811
1847
|
if node.is_a?(Prism::StatementsNode)
|
|
1812
1848
|
local_visibility = current_visibility
|
|
1813
|
-
node.
|
|
1849
|
+
node.rigor_each_child do |child|
|
|
1814
1850
|
local_visibility = walk_method_visibilities(child, qualified_prefix, in_singleton_class,
|
|
1815
1851
|
local_visibility, accumulator)
|
|
1816
1852
|
end
|
|
1817
1853
|
else
|
|
1818
|
-
node.
|
|
1854
|
+
node.rigor_each_child do |child|
|
|
1819
1855
|
walk_method_visibilities(child, qualified_prefix, in_singleton_class, current_visibility, accumulator)
|
|
1820
1856
|
end
|
|
1821
1857
|
end
|
|
1822
1858
|
current_visibility
|
|
1823
1859
|
end
|
|
1824
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/
|
|
1860
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1825
1861
|
|
|
1826
1862
|
def record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
|
|
1827
1863
|
return if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
|
|
@@ -1922,7 +1958,7 @@ module Rigor
|
|
|
1922
1958
|
return accumulator
|
|
1923
1959
|
end
|
|
1924
1960
|
|
|
1925
|
-
node.
|
|
1961
|
+
node.rigor_each_child { |child| collect_class_alias_map(child, qualified_prefix, accumulator) }
|
|
1926
1962
|
accumulator
|
|
1927
1963
|
end
|
|
1928
1964
|
|
|
@@ -2031,9 +2067,7 @@ module Rigor
|
|
|
2031
2067
|
# @return [Hash{Symbol => Hash}]
|
|
2032
2068
|
# `{ def_nodes:, def_sources:, superclasses:, includes:, class_sources: }`
|
|
2033
2069
|
def discovered_def_index_for_paths(paths, buffer: nil)
|
|
2034
|
-
acc =
|
|
2035
|
-
method_visibilities: {}, methods: {}, class_sources: {}, data_member_layouts: {},
|
|
2036
|
-
struct_member_layouts: {} }
|
|
2070
|
+
acc = new_def_index_accumulator
|
|
2037
2071
|
paths.each do |path|
|
|
2038
2072
|
physical = buffer ? buffer.resolve(path) : path
|
|
2039
2073
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
@@ -2042,13 +2076,391 @@ module Rigor
|
|
|
2042
2076
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
2043
2077
|
next
|
|
2044
2078
|
end
|
|
2079
|
+
finalize_def_index(acc)
|
|
2080
|
+
end
|
|
2081
|
+
|
|
2082
|
+
# B1 / ADR-89 WD1 (incremental propagation gates) — parses `paths` ONCE and returns the merged def-index
|
|
2083
|
+
# (as {#discovered_def_index_for_paths}), each file's comment-stripped {#code_fingerprint} (B1), AND each
|
|
2084
|
+
# file's {#declaration_signature} (ADR-89 WD1 — the per-def SIGNATURE shape that replaces the per-def body
|
|
2085
|
+
# fingerprint, so a body edit that leaves every signature equal is declaration-stable). The incremental
|
|
2086
|
+
# session drives the fingerprint / class-declaration change detection off `def_index` and the
|
|
2087
|
+
# declaration-stability skip decision off `declaration_signatures`, so a changed file is parsed once for
|
|
2088
|
+
# all of them (recon §2 dedup). A per-file live index (built + folded, exactly as
|
|
2089
|
+
# {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
|
|
2090
|
+
# reads their parameter structure from.
|
|
2091
|
+
# @return [Hash{Symbol => Object}] `{ def_index:, code_fingerprints:, declaration_signatures: }`.
|
|
2092
|
+
def scan_summary_for_paths(paths, buffer: nil)
|
|
2093
|
+
acc = new_def_index_accumulator
|
|
2094
|
+
code_fingerprints = {}
|
|
2095
|
+
declaration_signatures = {}
|
|
2096
|
+
paths.each do |path|
|
|
2097
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
2098
|
+
source = File.read(physical)
|
|
2099
|
+
parsed = Prism.parse(source, filepath: path)
|
|
2100
|
+
file_index = build_file_index(path, parsed.value)
|
|
2101
|
+
fold_file_index(acc, file_index)
|
|
2102
|
+
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
2103
|
+
declaration_signatures[path] = declaration_signature(file_index)
|
|
2104
|
+
rescue StandardError
|
|
2105
|
+
next
|
|
2106
|
+
end
|
|
2107
|
+
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
2108
|
+
declaration_signatures: declaration_signatures }
|
|
2109
|
+
end
|
|
2110
|
+
|
|
2111
|
+
# ADR-89 WD1 — a per-file digest of every cross-file DECLARATION surface an ancestry / file-level
|
|
2112
|
+
# dependent consumes, EXCLUDING method bodies (which only symbol dependents consume, via the ADR-46
|
|
2113
|
+
# symbol edges the change-detection already fingerprints per method). A body edit that touches no
|
|
2114
|
+
# signature leaves this equal, so the session drops the file's ancestry / file-level dependents; an
|
|
2115
|
+
# arity / visibility / added-or-removed-method / ancestry / member-layout edit moves it. Deliberately
|
|
2116
|
+
# SYNTACTIC (parameter structure from the def node, not inferred types).
|
|
2117
|
+
#
|
|
2118
|
+
# It captures, from a single file's live def-index: declared class/module names, superclass + include
|
|
2119
|
+
# ancestry, Data/Struct member layouts, the accessor/alias/define_method existence table, per-method
|
|
2120
|
+
# visibilities, and per method (instance + singleton) its name, parameter signature, AND def-start LINE.
|
|
2121
|
+
# The line is load-bearing for soundness: a body edit that SHIFTS a later def's line moves the ADR-17
|
|
2122
|
+
# `project_definition_site` a `call.undefined-method` consumer embeds, so a line shift must invalidate
|
|
2123
|
+
# the declaration (the file's dependents re-check) — this is why the signature is a superset of B1's
|
|
2124
|
+
# comment-stripped code fingerprint on line-moving edits, and strictly more permissive only on
|
|
2125
|
+
# same-line body edits (a local rename, an internal literal), which is the collapse WD1 exists for.
|
|
2126
|
+
def declaration_signature(file_index)
|
|
2127
|
+
parts = []
|
|
2128
|
+
append_ancestry_signature(parts, file_index)
|
|
2129
|
+
append_declaration_tables(parts, file_index)
|
|
2130
|
+
append_def_signatures(parts, file_index[:def_nodes], "#")
|
|
2131
|
+
append_def_signatures(parts, file_index[:singleton_def_nodes], ".")
|
|
2132
|
+
Digest::SHA256.hexdigest(parts.join("\x00"))
|
|
2133
|
+
end
|
|
2134
|
+
|
|
2135
|
+
# The class-declaration + ancestry + member-layout surface of the declaration signature (declared class
|
|
2136
|
+
# names, superclass / include ancestry, Data/Struct member layouts). Kept out of
|
|
2137
|
+
# {#declaration_signature} to hold its ABC budget.
|
|
2138
|
+
def append_ancestry_signature(parts, file_index)
|
|
2139
|
+
parts.concat(file_index[:class_sources].keys.sort_by(&:to_s).map { |cn| "c:#{cn}" })
|
|
2140
|
+
parts.concat(sorted_by_class(file_index[:superclasses]).map { |cn, sc| "s:#{cn}<#{sc}" })
|
|
2141
|
+
parts.concat(sorted_by_class(file_index[:includes]).map do |cn, mods|
|
|
2142
|
+
"i:#{cn}=#{Array(mods).map(&:to_s).sort.join(',')}"
|
|
2143
|
+
end)
|
|
2144
|
+
parts.concat(sorted_by_class(file_index[:data_member_layouts]).map { |cn, l| "d:#{cn}=#{l.inspect}" })
|
|
2145
|
+
parts.concat(sorted_by_class(file_index[:struct_member_layouts]).map { |cn, l| "t:#{cn}=#{l.inspect}" })
|
|
2146
|
+
end
|
|
2147
|
+
|
|
2148
|
+
# A class-keyed table's pairs sorted by class name (stringified) for a deterministic signature.
|
|
2149
|
+
def sorted_by_class(table)
|
|
2150
|
+
table.sort_by { |cn, _| cn.to_s }
|
|
2151
|
+
end
|
|
2152
|
+
|
|
2153
|
+
# The method-existence and visibility surfaces of the declaration signature (kept out of
|
|
2154
|
+
# {#declaration_signature} to hold its ABC budget). Both are consumed cross-file — the existence table
|
|
2155
|
+
# by undefined-method suppression, the visibilities by the ADR-35 override-visibility rule.
|
|
2156
|
+
def append_declaration_tables(parts, file_index)
|
|
2157
|
+
file_index[:methods].sort_by { |cn, _| cn.to_s }.each do |cn, table|
|
|
2158
|
+
table.sort_by { |m, _| m.to_s }.each { |m, kind| parts << "e:#{cn}##{m}=#{kind}" }
|
|
2159
|
+
end
|
|
2160
|
+
file_index[:method_visibilities].sort_by { |cn, _| cn.to_s }.each do |cn, table|
|
|
2161
|
+
table.sort_by { |m, _| m.to_s }.each { |m, vis| parts << "v:#{cn}##{m}=#{vis}" }
|
|
2162
|
+
end
|
|
2163
|
+
end
|
|
2164
|
+
|
|
2165
|
+
# Appends one def-node table's per-method signature entries (name + parameter structure + def-start
|
|
2166
|
+
# line) under `separator` (`#` instance / `.` singleton). Nodes here are always LIVE (both call sites —
|
|
2167
|
+
# {#scan_summary_for_paths} and {#build_seed_bundle} — pass a freshly parsed single-file index), so the
|
|
2168
|
+
# parameter structure and location are read directly.
|
|
2169
|
+
def append_def_signatures(parts, defs, separator)
|
|
2170
|
+
defs.sort_by { |cn, _| cn.to_s }.each do |class_name, methods|
|
|
2171
|
+
methods.sort_by { |m, _| m.to_s }.each do |method_name, node|
|
|
2172
|
+
parts << "m:#{class_name}#{separator}#{method_name}#{parameter_signature(node)}@#{def_start_line(node)}"
|
|
2173
|
+
end
|
|
2174
|
+
end
|
|
2175
|
+
end
|
|
2176
|
+
|
|
2177
|
+
# ADR-89 WD1 — a compact, order-preserving descriptor of a def node's parameter STRUCTURE: each
|
|
2178
|
+
# parameter's kind (required / optional / rest / post / keyword-required / keyword-optional /
|
|
2179
|
+
# keyword-rest / block / forwarding) and name, plus default-PRESENCE (implied by the optional kinds).
|
|
2180
|
+
# Names are included for all parameter positions: soundness-first (a keyword-name change is Liskov-
|
|
2181
|
+
# visible to overriders; a positional-name change is rare and only over-conservatively keeps a
|
|
2182
|
+
# dependent). Not the inferred parameter TYPE — this gate is deliberately syntactic.
|
|
2183
|
+
def parameter_signature(node)
|
|
2184
|
+
return "()" unless node.respond_to?(:parameters)
|
|
2185
|
+
|
|
2186
|
+
params = node.parameters
|
|
2187
|
+
return "()" if params.nil?
|
|
2188
|
+
|
|
2189
|
+
parts = []
|
|
2190
|
+
parts.concat(labelled_params(params.requireds, "r"))
|
|
2191
|
+
parts.concat(labelled_params(params.optionals, "o"))
|
|
2192
|
+
parts.concat(labelled_params(params.posts, "p"))
|
|
2193
|
+
parts.concat(labelled_params(params.keywords) { |p| keyword_param_kind(p) })
|
|
2194
|
+
parts.concat(rest_param_parts(params))
|
|
2195
|
+
"(#{parts.join(',')})"
|
|
2196
|
+
end
|
|
2197
|
+
|
|
2198
|
+
# The `<kind>:<name>` labels for a list of parameter nodes (nil → none). `kind` is a fixed String or a
|
|
2199
|
+
# block computing it per node (for the keyword-required / keyword-optional split).
|
|
2200
|
+
def labelled_params(nodes, kind = nil)
|
|
2201
|
+
(nodes || []).map { |p| "#{kind || yield(p)}:#{param_label(p)}" }
|
|
2202
|
+
end
|
|
2203
|
+
|
|
2204
|
+
# The single rest / keyword-rest / block parameter labels (each present at most once).
|
|
2205
|
+
def rest_param_parts(params)
|
|
2206
|
+
parts = []
|
|
2207
|
+
parts << "*:#{param_label(params.rest)}" if params.rest
|
|
2208
|
+
parts << "**:#{param_label(params.keyword_rest)}" if params.keyword_rest
|
|
2209
|
+
parts << "&:#{param_label(params.block)}" if params.block
|
|
2210
|
+
parts
|
|
2211
|
+
end
|
|
2212
|
+
|
|
2213
|
+
def keyword_param_kind(param)
|
|
2214
|
+
param.is_a?(Prism::OptionalKeywordParameterNode) ? "ko" : "kr"
|
|
2215
|
+
end
|
|
2216
|
+
|
|
2217
|
+
# A parameter node's name, or a class-tagged sentinel for the nameless / destructuring forms
|
|
2218
|
+
# (`MultiTargetNode`, an anonymous `*` / `**` / `&`, `NoKeywordsParameterNode`, `ForwardingParameterNode`).
|
|
2219
|
+
def param_label(param)
|
|
2220
|
+
return "" if param.nil?
|
|
2221
|
+
|
|
2222
|
+
param.respond_to?(:name) && param.name ? param.name.to_s : param.class.name.split("::").last
|
|
2223
|
+
end
|
|
2224
|
+
|
|
2225
|
+
# The 1-based start line of a live def node (the `project_definition_site` an ADR-17 consumer embeds).
|
|
2226
|
+
def def_start_line(node)
|
|
2227
|
+
node.respond_to?(:location) ? node.location.start_line : 0
|
|
2228
|
+
end
|
|
2229
|
+
|
|
2230
|
+
# B1 — the SHA-256 of `source` with every comment's byte range excised (a line comment ends before its
|
|
2231
|
+
# newline, so the newline is KEPT: stripping it preserves the line count, hence every def's start line
|
|
2232
|
+
# and the whole engine bundle). Two revisions of a file that differ ONLY in comment text therefore share
|
|
2233
|
+
# a code fingerprint — the signal the bundle-equality gate uses to prove a changed file's *code* (and so
|
|
2234
|
+
# every code-derived cross-file fact the engine and code-reading plugins produce from it) is unchanged.
|
|
2235
|
+
# A comment that adds or removes a LINE shifts subsequent code, so the fingerprint changes (the gate then
|
|
2236
|
+
# conservatively keeps the file's dependents — a line shift can move a def the ADR-17 diagnostic names).
|
|
2237
|
+
def code_fingerprint(source, comments)
|
|
2238
|
+
bytes = source.b
|
|
2239
|
+
return Digest::SHA256.hexdigest(bytes) if comments.empty?
|
|
2240
|
+
|
|
2241
|
+
result = +"".b
|
|
2242
|
+
cursor = 0
|
|
2243
|
+
comments.sort_by { |comment| comment.location.start_offset }.each do |comment|
|
|
2244
|
+
loc = comment.location
|
|
2245
|
+
result << bytes.byteslice(cursor, loc.start_offset - cursor) if loc.start_offset > cursor
|
|
2246
|
+
cursor = loc.end_offset if loc.end_offset > cursor
|
|
2247
|
+
end
|
|
2248
|
+
result << bytes.byteslice(cursor, bytes.bytesize - cursor)
|
|
2249
|
+
Digest::SHA256.hexdigest(result)
|
|
2250
|
+
end
|
|
2251
|
+
|
|
2252
|
+
# Combined single-parse cross-file pre-pass used by the project-wide runner pre-pass
|
|
2253
|
+
# ({Analysis::Runner::ProjectPrePasses#discover}). {#discovered_classes_for_paths} and
|
|
2254
|
+
# {#discovered_def_index_for_paths} each `Prism.parse` every project file independently; this walks the
|
|
2255
|
+
# project ONCE, parsing each file a single time and driving BOTH collectors over the same tree, then
|
|
2256
|
+
# returns `{ classes:, def_index: }`. Each file is parsed, collected, and dropped before the next
|
|
2257
|
+
# iteration, so no more than one AST is held alive at a time (peak RSS stays flat on large projects).
|
|
2258
|
+
#
|
|
2259
|
+
# Error degradation is identical to the two independent loops it replaces: a read / parse failure (the
|
|
2260
|
+
# rescue's real target) contributes nothing to either table. The subset-scoped callers
|
|
2261
|
+
# ({IncrementalSession}, `coverage --protection`) keep calling the individual methods unchanged.
|
|
2262
|
+
#
|
|
2263
|
+
# @param paths [Array<String>] project file paths.
|
|
2264
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
2265
|
+
# @return [Hash{Symbol => Object}] `{ classes: Hash, def_index: Hash }`.
|
|
2266
|
+
def discovered_project_index_for_paths(paths, buffer: nil)
|
|
2267
|
+
classes = {}
|
|
2268
|
+
acc = new_def_index_accumulator
|
|
2269
|
+
paths.each do |path|
|
|
2270
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
2271
|
+
root = Prism.parse(File.read(physical), filepath: path).value
|
|
2272
|
+
collect_class_decls(root, [], classes)
|
|
2273
|
+
accumulate_project_index(acc, path, root)
|
|
2274
|
+
rescue StandardError
|
|
2275
|
+
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
2276
|
+
next
|
|
2277
|
+
end
|
|
2278
|
+
{ classes: classes.freeze, def_index: finalize_def_index(acc) }
|
|
2279
|
+
end
|
|
2280
|
+
|
|
2281
|
+
# ADR-85 WD2 — the incremental cross-file discovery pass. The bundle-driven twin of
|
|
2282
|
+
# {#discovered_project_index_for_paths}: instead of parsing + walking every file, it folds each file's
|
|
2283
|
+
# cached per-file *seed bundle* (plain-data tables + `(node_id, name, fingerprint)` def-node handles),
|
|
2284
|
+
# re-walking ONLY the files whose current digest does not match their cached bundle (changed / added).
|
|
2285
|
+
# Rebuilds in canonical file order — the sound reconstruction (the merged tables are Set-union / later-wins
|
|
2286
|
+
# / whole-project-finalize, so a changed file cannot be delta-patched, only re-folded in place; recon Q2).
|
|
2287
|
+
#
|
|
2288
|
+
# Returns `{ classes:, def_index:, bundles: }` — `bundles` is the CURRENT per-file bundle set (cached ones
|
|
2289
|
+
# reused, changed ones refreshed, removed ones absent) so the caller persists the up-to-date snapshot.
|
|
2290
|
+
# A re-walked file's methods stay LIVE `Prism::DefNode`s in the returned index (no on-demand re-parse for
|
|
2291
|
+
# a file we just walked); an unchanged file's methods are {DefHandle}s the accessor choke points resolve
|
|
2292
|
+
# lazily. On a cold run (`seed_bundles` empty) every file is re-walked, so the index is entirely live —
|
|
2293
|
+
# identical to {#discovered_project_index_for_paths} — while the bundles are built for the next run.
|
|
2294
|
+
#
|
|
2295
|
+
# @param paths [Array<String>] project file paths, in canonical order.
|
|
2296
|
+
# @param seed_bundles [Hash{String => Hash}] the prior run's per-file bundles, keyed by logical path.
|
|
2297
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
2298
|
+
# @return [Hash{Symbol => Object}] `{ classes:, def_index:, bundles: }`.
|
|
2299
|
+
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil)
|
|
2300
|
+
classes = {}
|
|
2301
|
+
acc = new_def_index_accumulator
|
|
2302
|
+
bundles = {}
|
|
2303
|
+
paths.each do |path|
|
|
2304
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
2305
|
+
digest = Cache::FileDigest.hexdigest(physical)
|
|
2306
|
+
cached = seed_bundles[path]
|
|
2307
|
+
if cached && cached[:digest] == digest
|
|
2308
|
+
bundles[path] = cached
|
|
2309
|
+
classes.merge!(cached[:classes])
|
|
2310
|
+
fold_file_index(acc, bundle_to_file_index(cached, path))
|
|
2311
|
+
else
|
|
2312
|
+
source = File.read(physical)
|
|
2313
|
+
parsed = Prism.parse(source, filepath: path)
|
|
2314
|
+
root = parsed.value
|
|
2315
|
+
file_index = build_file_index(path, root)
|
|
2316
|
+
file_classes = {}
|
|
2317
|
+
collect_class_decls(root, [], file_classes)
|
|
2318
|
+
bundles[path] = build_seed_bundle(file_index, file_classes, digest,
|
|
2319
|
+
code_fingerprint(source, parsed.comments))
|
|
2320
|
+
classes.merge!(file_classes)
|
|
2321
|
+
fold_file_index(acc, file_index)
|
|
2322
|
+
end
|
|
2323
|
+
rescue StandardError
|
|
2324
|
+
# Skip files that fail to parse / read; the per-file analyzer surfaces the parse error separately.
|
|
2325
|
+
next
|
|
2326
|
+
end
|
|
2327
|
+
{ classes: classes.freeze, def_index: finalize_def_index(acc), bundles: bundles }
|
|
2328
|
+
end
|
|
2329
|
+
|
|
2330
|
+
# Builds ONE file's isolated def-index contribution (live `Prism::DefNode`s) by folding it into a fresh
|
|
2331
|
+
# accumulator — recon Q2(a)'s "constructible in isolation" property. Shared by the cold-baseline bundle
|
|
2332
|
+
# build and the incremental changed-file re-walk.
|
|
2333
|
+
def build_file_index(path, root)
|
|
2334
|
+
file_acc = new_def_index_accumulator
|
|
2335
|
+
accumulate_project_index(file_acc, path, root)
|
|
2336
|
+
file_acc
|
|
2337
|
+
end
|
|
2338
|
+
|
|
2339
|
+
# ADR-85 WD2 — folds a single file's isolated def-index contribution into the cross-file accumulator,
|
|
2340
|
+
# applying EXACTLY the merge semantics {#accumulate_project_index} applies incrementally (def_nodes
|
|
2341
|
+
# later-wins, def_sources first-wins, includes / class_sources accumulate, everything else later-wins).
|
|
2342
|
+
# Polymorphic over the def-node value: a re-walked file's `file_index` carries live nodes; a cached
|
|
2343
|
+
# bundle's carries {DefHandle}s. The merges never deref the value, so both fold identically.
|
|
2344
|
+
def fold_file_index(acc, file_index)
|
|
2345
|
+
fold_def_tables(acc, file_index)
|
|
2346
|
+
fold_ancestry_tables(acc, file_index)
|
|
2347
|
+
end
|
|
2348
|
+
|
|
2349
|
+
# def_nodes / singleton_def_nodes / method_visibilities / methods fold class-nested later-wins;
|
|
2350
|
+
# def_sources / singleton_def_sources fold first-wins ({#fold_def_sources}).
|
|
2351
|
+
def fold_def_tables(acc, file_index)
|
|
2352
|
+
file_index[:def_nodes].each { |cn, methods| (acc[:def_nodes][cn] ||= {}).merge!(methods) }
|
|
2353
|
+
file_index[:singleton_def_nodes].each { |cn, methods| (acc[:singleton_def_nodes][cn] ||= {}).merge!(methods) }
|
|
2354
|
+
file_index[:method_visibilities].each { |cn, table| (acc[:method_visibilities][cn] ||= {}).merge!(table) }
|
|
2355
|
+
file_index[:methods].each { |cn, table| (acc[:methods][cn] ||= {}).merge!(table) }
|
|
2356
|
+
fold_def_sources(acc, :def_sources, file_index[:def_sources])
|
|
2357
|
+
fold_def_sources(acc, :singleton_def_sources, file_index[:singleton_def_sources])
|
|
2358
|
+
end
|
|
2359
|
+
|
|
2360
|
+
# A `"path:line"` source table (instance or singleton) is first-file-wins per `(class, method)` (`||=`),
|
|
2361
|
+
# matching `merge_discovered_defs`.
|
|
2362
|
+
def fold_def_sources(acc, key, file_sources)
|
|
2363
|
+
file_sources.each do |cn, methods|
|
|
2364
|
+
target = (acc[key][cn] ||= {})
|
|
2365
|
+
methods.each { |method_name, source| target[method_name] ||= source }
|
|
2366
|
+
end
|
|
2367
|
+
end
|
|
2368
|
+
|
|
2369
|
+
# superclasses later-wins; includes / class_sources accumulate; member layouts later-wins.
|
|
2370
|
+
def fold_ancestry_tables(acc, file_index)
|
|
2371
|
+
acc[:superclasses].merge!(file_index[:superclasses])
|
|
2372
|
+
file_index[:includes].each { |cn, mods| acc[:includes][cn] = ((acc[:includes][cn] || []) + mods).uniq }
|
|
2373
|
+
file_index[:class_sources].each { |cn, files| (acc[:class_sources][cn] ||= Set.new).merge(files) }
|
|
2374
|
+
acc[:data_member_layouts].merge!(file_index[:data_member_layouts])
|
|
2375
|
+
acc[:struct_member_layouts].merge!(file_index[:struct_member_layouts])
|
|
2376
|
+
end
|
|
2377
|
+
|
|
2378
|
+
# ADR-85 WD2 — converts a file's live single-file index + its class table into a Marshal-clean seed
|
|
2379
|
+
# bundle: the plain-data tables verbatim, the def-node tables re-expressed as `[node_id, name,
|
|
2380
|
+
# fingerprint]` triples (the path is the bundle key), the class-source names (path implicit), and the
|
|
2381
|
+
# content digest that gates the bundle's reuse.
|
|
2382
|
+
def build_seed_bundle(file_index, file_classes, digest, code_fingerprint)
|
|
2383
|
+
{
|
|
2384
|
+
digest: digest,
|
|
2385
|
+
# B1 — the comment-stripped code fingerprint, so a recheck can prove this file's edit was
|
|
2386
|
+
# comment-only and skip its dependents.
|
|
2387
|
+
code_fingerprint: code_fingerprint,
|
|
2388
|
+
# ADR-89 WD1 — the per-def SIGNATURE-shape declaration signature (bodies excluded, def lines kept),
|
|
2389
|
+
# so a recheck can prove this file's body edit changed no declaration and skip its ancestry /
|
|
2390
|
+
# file-level dependents. Computed from the same live `file_index` the bundle is built from, so the
|
|
2391
|
+
# value stored here equals the one a later recheck recomputes for an unchanged declaration.
|
|
2392
|
+
declaration_signature: declaration_signature(file_index),
|
|
2393
|
+
classes: file_classes,
|
|
2394
|
+
def_nodes: live_defs_to_bundle(file_index[:def_nodes]),
|
|
2395
|
+
singleton_def_nodes: live_defs_to_bundle(file_index[:singleton_def_nodes]),
|
|
2396
|
+
def_sources: file_index[:def_sources],
|
|
2397
|
+
singleton_def_sources: file_index[:singleton_def_sources],
|
|
2398
|
+
superclasses: file_index[:superclasses],
|
|
2399
|
+
includes: file_index[:includes],
|
|
2400
|
+
method_visibilities: file_index[:method_visibilities],
|
|
2401
|
+
methods: file_index[:methods],
|
|
2402
|
+
class_source_names: file_index[:class_sources].keys,
|
|
2403
|
+
data_member_layouts: file_index[:data_member_layouts],
|
|
2404
|
+
struct_member_layouts: file_index[:struct_member_layouts]
|
|
2405
|
+
}
|
|
2406
|
+
end
|
|
2407
|
+
|
|
2408
|
+
# ADR-85 WD2 — reconstitutes a cached bundle into a single-file index {#fold_file_index} folds: the
|
|
2409
|
+
# def-node triples become {DefHandle}s bound to this file's `path`, and the class-source names become a
|
|
2410
|
+
# `{name => Set[path]}` table (the shape `accumulate_project_index` produces).
|
|
2411
|
+
def bundle_to_file_index(bundle, path)
|
|
2412
|
+
{
|
|
2413
|
+
def_nodes: bundle_defs_to_handles(bundle[:def_nodes], path),
|
|
2414
|
+
singleton_def_nodes: bundle_defs_to_handles(bundle[:singleton_def_nodes], path),
|
|
2415
|
+
def_sources: bundle[:def_sources],
|
|
2416
|
+
# ADR-85 schema 7 — a pre-7 bundle lacks this key; the SCHEMA bump loads it as a clean cold rebuild,
|
|
2417
|
+
# but default to `{}` so any in-flight fold stays total.
|
|
2418
|
+
singleton_def_sources: bundle[:singleton_def_sources] || {},
|
|
2419
|
+
superclasses: bundle[:superclasses],
|
|
2420
|
+
includes: bundle[:includes],
|
|
2421
|
+
method_visibilities: bundle[:method_visibilities],
|
|
2422
|
+
methods: bundle[:methods],
|
|
2423
|
+
class_sources: bundle[:class_source_names].to_h { |name| [name, Set[path]] },
|
|
2424
|
+
data_member_layouts: bundle[:data_member_layouts],
|
|
2425
|
+
struct_member_layouts: bundle[:struct_member_layouts]
|
|
2426
|
+
}
|
|
2427
|
+
end
|
|
2428
|
+
|
|
2429
|
+
# `{class => {method => Prism::DefNode}}` → `{class => {method => [node_id, name, fingerprint]}}`.
|
|
2430
|
+
def live_defs_to_bundle(defs)
|
|
2431
|
+
defs.transform_values do |methods|
|
|
2432
|
+
methods.transform_values do |node|
|
|
2433
|
+
[node.node_id, node.name.to_s, Digest::SHA256.hexdigest(node.location.slice)]
|
|
2434
|
+
end
|
|
2435
|
+
end
|
|
2436
|
+
end
|
|
2437
|
+
|
|
2438
|
+
# `{class => {method => [node_id, name, fingerprint]}}` → `{class => {method => DefHandle}}` for `path`.
|
|
2439
|
+
def bundle_defs_to_handles(defs, path)
|
|
2440
|
+
defs.transform_values do |methods|
|
|
2441
|
+
methods.transform_values do |(node_id, name, fingerprint)|
|
|
2442
|
+
DefHandle.new(path: path, node_id: node_id, name: name, fingerprint: fingerprint)
|
|
2443
|
+
end
|
|
2444
|
+
end
|
|
2445
|
+
end
|
|
2446
|
+
|
|
2447
|
+
# The empty per-run accumulator the def-index passes fold each file into.
|
|
2448
|
+
def new_def_index_accumulator
|
|
2449
|
+
{ def_nodes: {}, singleton_def_nodes: {}, def_sources: {}, singleton_def_sources: {},
|
|
2450
|
+
superclasses: {}, includes: {}, method_visibilities: {}, methods: {}, class_sources: {},
|
|
2451
|
+
data_member_layouts: {}, struct_member_layouts: {} }
|
|
2452
|
+
end
|
|
2453
|
+
|
|
2454
|
+
# Post-processes and freezes a fully-folded def-index accumulator.
|
|
2455
|
+
def finalize_def_index(acc)
|
|
2045
2456
|
# Cross-file method suppression is for the project's OWN accessors (attr_* / define_method / alias) — NOT for
|
|
2046
2457
|
# plain `def`s. A cross-file `def` on a class is exactly the ADR-17 monkey-patch case the undefined-method rule
|
|
2047
2458
|
# deliberately surfaces (fire + def-site annotation, nudging `pre_eval:`), so dropping the `def`-declared names
|
|
2048
2459
|
# keeps that contract intact while still letting `attr_reader :x` in one file suppress a false undefined-method
|
|
2049
2460
|
# for `obj.x` in another.
|
|
2050
2461
|
acc[:methods] = subtract_def_methods(acc[:methods], acc[:def_nodes])
|
|
2051
|
-
%i[def_nodes singleton_def_nodes def_sources includes method_visibilities
|
|
2462
|
+
%i[def_nodes singleton_def_nodes def_sources singleton_def_sources includes method_visibilities
|
|
2463
|
+
methods class_sources].each do |key|
|
|
2052
2464
|
acc[key].each_value(&:freeze)
|
|
2053
2465
|
end
|
|
2054
2466
|
acc.transform_values(&:freeze)
|
|
@@ -2073,9 +2485,12 @@ module Rigor
|
|
|
2073
2485
|
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
2074
2486
|
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
2075
2487
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2488
|
+
# ADR-46 slice 4 (singleton) — record the singleton-side `"path:line"` sources alongside the nodes,
|
|
2489
|
+
# the exact mirror of the instance-side `merge_discovered_defs`, so a class/singleton-method body edit
|
|
2490
|
+
# produces a changed `"Class.method"` fingerprint pair (and its call sites a symbol edge) instead of
|
|
2491
|
+
# silently degrading to the file's full ancestry closure.
|
|
2492
|
+
merge_discovered_defs(acc[:singleton_def_nodes], acc[:singleton_def_sources], path,
|
|
2493
|
+
build_discovered_singleton_def_nodes(root))
|
|
2079
2494
|
superclasses = build_discovered_superclasses(root)
|
|
2080
2495
|
includes = build_discovered_includes(root)
|
|
2081
2496
|
acc[:superclasses].merge!(superclasses)
|
|
@@ -2152,7 +2567,7 @@ module Rigor
|
|
|
2152
2567
|
record_class_new_constant_decl(node, qualified_prefix, accumulator)
|
|
2153
2568
|
end
|
|
2154
2569
|
|
|
2155
|
-
node.
|
|
2570
|
+
node.rigor_each_child { |child| collect_class_decls(child, qualified_prefix, accumulator) }
|
|
2156
2571
|
end
|
|
2157
2572
|
|
|
2158
2573
|
# T1 (template-corpora survey) — record a `Const = Class.new(Super)` (and the bare `Class.new` / `Module.new`)
|
|
@@ -2219,7 +2634,7 @@ module Rigor
|
|
|
2219
2634
|
return if record_meta_new_constant?(node, qualified_prefix, identity_table, discovered)
|
|
2220
2635
|
end
|
|
2221
2636
|
|
|
2222
|
-
node.
|
|
2637
|
+
node.rigor_each_child do |child|
|
|
2223
2638
|
record_declarations(child, qualified_prefix, identity_table, discovered)
|
|
2224
2639
|
end
|
|
2225
2640
|
end
|
|
@@ -2345,7 +2760,7 @@ module Rigor
|
|
|
2345
2760
|
when Prism::UnlessNode
|
|
2346
2761
|
propagate_unless_branches(node, table, current_scope)
|
|
2347
2762
|
else
|
|
2348
|
-
node.
|
|
2763
|
+
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
2349
2764
|
end
|
|
2350
2765
|
end
|
|
2351
2766
|
|