rigortype 0.1.19 → 0.2.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 +41 -6
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +138 -16
- data/lib/rigor/cli/coverage_command.rb +138 -31
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +57 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +76 -5
- data/lib/rigor/environment.rb +66 -8
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +271 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +180 -0
- data/lib/rigor/protection/mutator.rb +267 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +49 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../scope"
|
|
6
|
+
require_relative "../inference/scope_indexer"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
# ADR-63 Tier 2 — the productized subset of the dev-only mutation-testing
|
|
10
|
+
# harness (`tool/mutation/`, ADR-62). Only the *per-file effectiveness
|
|
11
|
+
# measurement* lives here — the type-visible {Mutator} and the warm-loop
|
|
12
|
+
# {MutationScanner} kill-rate measurement. The dev sweep / fuzz / survivor
|
|
13
|
+
# clustering stay off the frozen surface in `tool/mutation/mutate.rb`
|
|
14
|
+
# (which now reuses this {Mutator} so there is one source of truth).
|
|
15
|
+
module Protection
|
|
16
|
+
# One concrete edit: replace source bytes [start, stop) with `replacement`.
|
|
17
|
+
# `anchor` is the Prism node whose inferred type decides type-relevance —
|
|
18
|
+
# the call receiver whose contract the mutation could violate, or nil when
|
|
19
|
+
# there is no concrete receiver (implicit-self call, literal outside a call).
|
|
20
|
+
# `anchor_type` (the rendered receiver type) and `method_name` are filled in
|
|
21
|
+
# for reporting a surviving site; both may stay nil.
|
|
22
|
+
Mutation = Struct.new(
|
|
23
|
+
:operator, :expected_rule, :start, :stop, :replacement, :line, :label, :anchor,
|
|
24
|
+
:anchor_type, :method_name,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
) do
|
|
27
|
+
def apply(source)
|
|
28
|
+
prefix = source.byteslice(0, start)
|
|
29
|
+
suffix = source.byteslice(stop, source.bytesize - stop) || ""
|
|
30
|
+
"#{prefix}#{replacement}#{suffix}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Generates type-visible mutations of a Ruby source string by walking the
|
|
35
|
+
# Prism AST and recording byte-range splices (no unparser needed — Prism
|
|
36
|
+
# hands us exact offsets, and the analyzer re-parses the spliced source).
|
|
37
|
+
#
|
|
38
|
+
# A mutation is "type-visible" when it should trip a diagnostic rule *if*
|
|
39
|
+
# Rigor holds a type at the site: a call-argument literal dropped to `nil`
|
|
40
|
+
# or type-swapped (→ `call.argument-type-mismatch`), or a call site renamed
|
|
41
|
+
# to a missing method (→ `call.undefined-method`). Only call sites and
|
|
42
|
+
# bodies are mutated, never `def` signatures, so a reused project scan stays
|
|
43
|
+
# valid.
|
|
44
|
+
class Mutator
|
|
45
|
+
IDENT = /\A[a-z_][A-Za-z0-9_]*\z/
|
|
46
|
+
QUOTES = ['"', "'"].freeze
|
|
47
|
+
# Mutating an argument to a universal-equality method is always an
|
|
48
|
+
# equivalent mutant: Ruby's `==` / `<=>` family returns false / nil on a
|
|
49
|
+
# type mismatch rather than raising, so the engine exempts them
|
|
50
|
+
# (`UNIVERSAL_EQUALITY_METHODS`). Skip them to keep survivors meaningful.
|
|
51
|
+
UNIVERSAL_EQUALITY = %w[== != eql? equal? <=>].freeze
|
|
52
|
+
|
|
53
|
+
# Every operator the mutator knows. Each maps to the diagnostic rule
|
|
54
|
+
# family it is *engineered* to trip when the mutated value/call sits in a
|
|
55
|
+
# context where Rigor has type knowledge.
|
|
56
|
+
ALL_OPERATORS = %i[nil_inject type_swap undefined_method arity_extra].freeze
|
|
57
|
+
|
|
58
|
+
# The default set. `arity_extra` is excluded: most Ruby methods accept an
|
|
59
|
+
# extra argument (splat / optional), so appending one is usually an
|
|
60
|
+
# equivalent mutant — it contributes almost only noise. Re-enable it
|
|
61
|
+
# explicitly via `operators:` to measure arity teeth. (A signature-arity
|
|
62
|
+
# guard would make it default-worthy — a follow-up.)
|
|
63
|
+
OPERATORS = %i[nil_inject type_swap undefined_method].freeze
|
|
64
|
+
|
|
65
|
+
def initialize(source, operators: OPERATORS)
|
|
66
|
+
@source = source
|
|
67
|
+
@operators = operators
|
|
68
|
+
@parse = Prism.parse(source)
|
|
69
|
+
@anchor_for = {} # literal node -> its enclosing call's receiver node (or nil)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def mutations
|
|
73
|
+
return [] unless @parse.success?
|
|
74
|
+
|
|
75
|
+
index_literal_anchors(@parse.value)
|
|
76
|
+
out = []
|
|
77
|
+
walk(@parse.value) { |node| collect(node, out) }
|
|
78
|
+
out
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Phase 1.5 — keep only mutations whose anchor types to a concrete,
|
|
82
|
+
# non-Dynamic type, i.e. a site where Rigor actually holds a contract the
|
|
83
|
+
# mutation could violate. Drops implicit-self calls and literals outside a
|
|
84
|
+
# typed call (no contract → guaranteed survival → noise). FP-safe
|
|
85
|
+
# direction: an unresolved/probe-failed type KEEPS the mutation, so the
|
|
86
|
+
# filter never hides a kill it is unsure about — it only removes
|
|
87
|
+
# provably-Dynamic sites. Returns [kept, dropped_count]. Builds the scope
|
|
88
|
+
# index from THIS mutator's parse so anchor node identity matches the keys.
|
|
89
|
+
def filter_by_type(mutations, environment:, path:)
|
|
90
|
+
base = Rigor::Scope.empty(environment: environment, source_path: path)
|
|
91
|
+
index = Rigor::Inference::ScopeIndexer.index(@parse.value, default_scope: base)
|
|
92
|
+
cache = {}
|
|
93
|
+
kept = mutations.select do |mut|
|
|
94
|
+
keep, type = anchor_decision(mut.anchor, index, cache)
|
|
95
|
+
mut.anchor_type = type if keep
|
|
96
|
+
keep
|
|
97
|
+
end
|
|
98
|
+
[kept, mutations.size - kept.size]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# ADR-69 Seam 2 (AllSites) — keep every *dispatch-site* mutation (a method
|
|
102
|
+
# call or a call-argument literal), Dynamic receiver included, annotating
|
|
103
|
+
# the anchor type where Rigor holds one. Drops only non-dispatch literals
|
|
104
|
+
# (a literal outside any call — no receiver contract to violate). The
|
|
105
|
+
# biteable {#filter_by_type} hides exactly the Dynamic sites a test-suite
|
|
106
|
+
# consumer most wants to probe: where Rigor cannot bite, a test is the only
|
|
107
|
+
# protection. Use only with a {TestSuiteOracle} — at a Dynamic site the
|
|
108
|
+
# type pass can never kill, so without the test axis these are all noise.
|
|
109
|
+
def dispatch_site_mutations(mutations, environment:, path:)
|
|
110
|
+
base = Rigor::Scope.empty(environment: environment, source_path: path)
|
|
111
|
+
index = Rigor::Inference::ScopeIndexer.index(@parse.value, default_scope: base)
|
|
112
|
+
cache = {}
|
|
113
|
+
mutations.select do |mut|
|
|
114
|
+
next false if mut.method_name.nil?
|
|
115
|
+
|
|
116
|
+
_keep, type = anchor_decision(mut.anchor, index, cache)
|
|
117
|
+
mut.anchor_type = type
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def walk(node, &blk)
|
|
125
|
+
return if node.nil?
|
|
126
|
+
|
|
127
|
+
blk.call(node)
|
|
128
|
+
node.compact_child_nodes.each { |child| walk(child, &blk) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def collect(node, out)
|
|
132
|
+
case node
|
|
133
|
+
when Prism::IntegerNode, Prism::FloatNode
|
|
134
|
+
literal_mutations(node, out, numeric: true)
|
|
135
|
+
when Prism::StringNode
|
|
136
|
+
literal_mutations(node, out, numeric: false)
|
|
137
|
+
when Prism::CallNode
|
|
138
|
+
call_mutations(node, out)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Record, for each literal that is a direct call argument, the receiver of
|
|
143
|
+
# the enclosing call — the anchor whose param contract a literal mutation
|
|
144
|
+
# could violate. Literals elsewhere get a nil anchor (filtered out under
|
|
145
|
+
# the type filter).
|
|
146
|
+
def index_literal_anchors(node)
|
|
147
|
+
return if node.nil?
|
|
148
|
+
|
|
149
|
+
if node.is_a?(Prism::CallNode) && node.arguments
|
|
150
|
+
node.arguments.arguments.each do |arg|
|
|
151
|
+
@anchor_for[arg] = [node.receiver, node.name.to_s] if literal?(arg)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
node.compact_child_nodes.each { |child| index_literal_anchors(child) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def literal?(node)
|
|
158
|
+
node.is_a?(Prism::IntegerNode) || node.is_a?(Prism::FloatNode) || node.is_a?(Prism::StringNode)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Returns [keep?, rendered_type]. Keep when `anchor` is a site where Rigor
|
|
162
|
+
# holds a concrete (non-Dynamic/Top) type. FP-safe: an unresolved or
|
|
163
|
+
# probe-failed type keeps the mutation (with a nil rendered type).
|
|
164
|
+
def anchor_decision(anchor, index, cache)
|
|
165
|
+
return [false, nil] if anchor.nil?
|
|
166
|
+
return cache[anchor] if cache.key?(anchor)
|
|
167
|
+
|
|
168
|
+
cache[anchor] = compute_anchor_decision(anchor, index)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def compute_anchor_decision(anchor, index)
|
|
172
|
+
scope = index[anchor]
|
|
173
|
+
return [true, nil] if scope.nil? # unresolved scope → keep (FP-safe)
|
|
174
|
+
|
|
175
|
+
type = scope.type_of(anchor)
|
|
176
|
+
return [true, nil] if type.nil?
|
|
177
|
+
|
|
178
|
+
concrete = !non_concrete_type?(type)
|
|
179
|
+
[concrete, concrete ? render_type(type) : nil]
|
|
180
|
+
rescue StandardError
|
|
181
|
+
[true, nil] # never let a probe failure hide a candidate
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# A receiver type Rigor cannot bite on, so a mutation anchored to it would
|
|
185
|
+
# survive as noise: `Dynamic` / `Top` / `bot`, or a union with any such arm
|
|
186
|
+
# (gradually valid — `Array | Dynamic[top]`.whatever never fires). A union
|
|
187
|
+
# of fully-concrete arms (`String | Symbol`) stays concrete — it now has
|
|
188
|
+
# undefined-method teeth.
|
|
189
|
+
def non_concrete_type?(type)
|
|
190
|
+
return true if type.is_a?(Rigor::Type::Dynamic) || type.is_a?(Rigor::Type::Top) ||
|
|
191
|
+
type.is_a?(Rigor::Type::Bot)
|
|
192
|
+
return type.members.any? { |member| non_concrete_type?(member) } if type.is_a?(Rigor::Type::Union)
|
|
193
|
+
|
|
194
|
+
false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def render_type(type)
|
|
198
|
+
type.respond_to?(:describe) ? type.describe(:short) : type.to_s
|
|
199
|
+
rescue StandardError
|
|
200
|
+
type.class.name
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Mutate a literal: drop it to nil (possible-nil channel) and swap its
|
|
204
|
+
# type (type-mismatch channel). String literals are only touched when the
|
|
205
|
+
# node is a real quoted string, so we never corrupt `%w[...]` words.
|
|
206
|
+
def literal_mutations(node, out, numeric:)
|
|
207
|
+
return if !numeric && !QUOTES.include?(node.opening_loc&.slice)
|
|
208
|
+
|
|
209
|
+
anchor, method = @anchor_for[node]
|
|
210
|
+
return if UNIVERSAL_EQUALITY.include?(method)
|
|
211
|
+
|
|
212
|
+
loc = node.location
|
|
213
|
+
add(out, :nil_inject, "call.argument-type-mismatch", loc.start_offset, loc.end_offset,
|
|
214
|
+
"nil", loc.start_line, "literal → nil (#{snippet(loc)})", anchor, method)
|
|
215
|
+
swap = numeric ? '"rigor_mutant"' : "0"
|
|
216
|
+
add(out, :type_swap, "call.argument-type-mismatch", loc.start_offset, loc.end_offset,
|
|
217
|
+
swap, loc.start_line, "literal type swap (#{snippet(loc)} → #{swap})", anchor, method)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def call_mutations(node, out)
|
|
221
|
+
rename_call(node, out)
|
|
222
|
+
extend_arity(node, out)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Rename the *call site* (not the def) to a method that cannot exist, so a
|
|
226
|
+
# typed receiver trips call.undefined-method. We leave `def` signatures
|
|
227
|
+
# untouched on purpose: the prebuilt ProjectScan still carries the file's
|
|
228
|
+
# original declarations, so mutating only bodies/call-sites keeps it valid.
|
|
229
|
+
# Anchor is the explicit receiver (nil ⇒ implicit self ⇒ filtered out, as
|
|
230
|
+
# call.self-undefined-method ships `:off`).
|
|
231
|
+
def rename_call(node, out)
|
|
232
|
+
name = node.name.to_s
|
|
233
|
+
mloc = node.message_loc
|
|
234
|
+
return unless mloc && IDENT.match?(name)
|
|
235
|
+
|
|
236
|
+
add(out, :undefined_method, "call.undefined-method", mloc.start_offset, mloc.end_offset,
|
|
237
|
+
"#{name}__rigor_absent", mloc.start_line, "call ##{name} → missing method", node.receiver, name)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Append a trailing argument inside explicit `(...)` parens to trip an
|
|
241
|
+
# arity diagnostic against a known fixed-arity signature.
|
|
242
|
+
def extend_arity(node, out)
|
|
243
|
+
open = node.opening_loc
|
|
244
|
+
close = node.closing_loc
|
|
245
|
+
return unless close && open&.slice == "("
|
|
246
|
+
|
|
247
|
+
args = node.arguments&.arguments
|
|
248
|
+
insertion = args && !args.empty? ? ", nil" : "nil"
|
|
249
|
+
add(out, :arity_extra, "call.wrong-arity", close.start_offset, close.start_offset,
|
|
250
|
+
insertion, node.location.start_line, "call ##{node.name} +1 arg", node.receiver, node.name.to_s)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def add(out, operator, rule, start, stop, replacement, line, label, anchor, method_name) # rubocop:disable Metrics/ParameterLists
|
|
254
|
+
return unless @operators.include?(operator)
|
|
255
|
+
|
|
256
|
+
out << Mutation.new(operator: operator, expected_rule: rule, start: start, stop: stop,
|
|
257
|
+
replacement: replacement, line: line, label: label, anchor: anchor,
|
|
258
|
+
method_name: method_name)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def snippet(loc)
|
|
262
|
+
text = loc.slice.gsub(/\s+/, " ")
|
|
263
|
+
text.length > 30 ? "#{text[0, 27]}..." : text
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Protection
|
|
5
|
+
# ADR-70 — the **test-suite** kill oracle, the dynamic sibling of
|
|
6
|
+
# {DiagnosticOracle} on the ADR-69 seam. A mutant is killed iff applying its
|
|
7
|
+
# bytes to the file under test turns the project's test suite **red**. This is
|
|
8
|
+
# the dynamic half of the fused static∪dynamic protection map: a `Dynamic`
|
|
9
|
+
# site Rigor cannot bite (a type survivor) may still be fully guarded by a
|
|
10
|
+
# test.
|
|
11
|
+
#
|
|
12
|
+
# The suite command is the **runner hook** (`--test-command`, e.g.
|
|
13
|
+
# `bundle exec rake`). The runner is injectable so the decision logic is
|
|
14
|
+
# unit-testable without shelling out; the default shells out and reads the
|
|
15
|
+
# process exit status (0 = green / passed).
|
|
16
|
+
#
|
|
17
|
+
# I/O policy: {#killed?} writes the mutant to disk, runs the suite, and
|
|
18
|
+
# **always restores** the original bytes in an `ensure` — a normal exception
|
|
19
|
+
# never leaves a mutant on disk. (A hard interrupt mid-suite is the standard
|
|
20
|
+
# mutation-testing hazard the `ensure` cannot cover; callers running this in
|
|
21
|
+
# CI accept that, as `mutant` / Stryker do.)
|
|
22
|
+
class TestSuiteOracle
|
|
23
|
+
# @param command [Array<String>] the test command (the runner hook)
|
|
24
|
+
# @param runner [#call, nil] `runner.call(command) -> true iff the suite
|
|
25
|
+
# passed`. Defaults to shelling out via `system`.
|
|
26
|
+
def initialize(command:, runner: nil)
|
|
27
|
+
@command = command
|
|
28
|
+
@runner = runner || method(:shell_run)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The baseline: the suite must pass on clean code, else "a mutant survived"
|
|
32
|
+
# is meaningless (every mutant would look killed, or none would). Run once
|
|
33
|
+
# before measuring.
|
|
34
|
+
def green?
|
|
35
|
+
@runner.call(@command)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Killed iff the mutant turns the suite red. Restores `original` afterward.
|
|
39
|
+
# @param path [String] the file to (temporarily) overwrite with the mutant
|
|
40
|
+
# @param original [String] the clean bytes to restore
|
|
41
|
+
# @param mutant_source [String] the mutated bytes to test against
|
|
42
|
+
def killed?(path:, original:, mutant_source:)
|
|
43
|
+
File.write(path, mutant_source)
|
|
44
|
+
!@runner.call(@command)
|
|
45
|
+
ensure
|
|
46
|
+
File.write(path, original)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Run the suite with Bundler's environment stripped, so a `bundle exec`
|
|
52
|
+
# test command resolves the **target** project's Gemfile — not whatever
|
|
53
|
+
# bundle Rigor itself was launched under. Running Rigor via `bundle exec`
|
|
54
|
+
# leaks `RUBYOPT=-rbundler/setup` + `GEM_HOME` / `BUNDLE_*` into a plain
|
|
55
|
+
# `system` subprocess, which then resolves the target's Gemfile against
|
|
56
|
+
# Rigor's gems and fails — so a green suite looks red and the run aborts.
|
|
57
|
+
# `with_unbundled_env` restores the pre-bundler env (a bare `env -u
|
|
58
|
+
# BUNDLE_GEMFILE` is not enough — the `BUNDLER_ORIG_*` preservers defeat
|
|
59
|
+
# it). Found validating ADR-70 on real projects (2026-06-17).
|
|
60
|
+
def shell_run(command)
|
|
61
|
+
run = -> { system(*command, out: File::NULL, err: File::NULL) }
|
|
62
|
+
return run.call unless defined?(Bundler) && Bundler.respond_to?(:with_unbundled_env)
|
|
63
|
+
|
|
64
|
+
Bundler.with_unbundled_env(&run)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -7,43 +7,30 @@ require_relative "rbs_extended/reporter"
|
|
|
7
7
|
require_relative "rbs_extended/hkt_directives"
|
|
8
8
|
|
|
9
9
|
module Rigor
|
|
10
|
-
#
|
|
11
|
-
# `RBS::Extended` annotation surface described in
|
|
10
|
+
# Reader for the `RBS::Extended` annotation surface described in
|
|
12
11
|
# `docs/type-specification/rbs-extended.md`.
|
|
13
12
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# consume. v0.0.2 recognises:
|
|
13
|
+
# Reads `%a{rigor:v1:<directive> <payload>}` annotations off RBS
|
|
14
|
+
# method definitions and returns well-typed effect objects the
|
|
15
|
+
# inference engine can consume. Implemented directives:
|
|
18
16
|
#
|
|
19
|
-
# - `rigor:v1:predicate-if-true <target> is <ClassName>`
|
|
20
|
-
# - `rigor:v1:predicate-if-false <target> is <ClassName>`
|
|
21
|
-
# - `rigor:v1:assert <target> is <ClassName>`
|
|
22
|
-
# - `rigor:v1:assert-if-true <target> is <ClassName>`
|
|
23
|
-
# - `rigor:v1:assert-if-false <target> is <ClassName>`
|
|
17
|
+
# - `rigor:v1:predicate-if-true <target> is <ClassName|refinement>`
|
|
18
|
+
# - `rigor:v1:predicate-if-false <target> is <ClassName|refinement>`
|
|
19
|
+
# - `rigor:v1:assert <target> is <ClassName|refinement>`
|
|
20
|
+
# - `rigor:v1:assert-if-true <target> is <ClassName|refinement>`
|
|
21
|
+
# - `rigor:v1:assert-if-false <target> is <ClassName|refinement>`
|
|
22
|
+
# - `rigor:v1:param <name> <type-expr>` — per-call param narrowing
|
|
23
|
+
# - `rigor:v1:return <type-expr>` — per-call return override
|
|
24
|
+
# - `rigor:v1:conforms-to <InterfaceName>` — structural conformance
|
|
24
25
|
#
|
|
25
|
-
# `predicate-if-*` fires when the call is used as an
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# `target: self` narrowing, ...) remain on the v0.0.x
|
|
34
|
-
# roadmap. Annotations whose key is in the `rigor:v1:`
|
|
35
|
-
# namespace but whose directive is unrecognised are
|
|
36
|
-
# silently ignored at first-preview quality (a future slice
|
|
37
|
-
# MAY surface them as diagnostics-on-Rigor-itself per the
|
|
38
|
-
# spec's "unsupported metadata" guidance).
|
|
39
|
-
#
|
|
40
|
-
# The parser is minimal: it accepts a strict shape
|
|
41
|
-
# `<target> is <ClassName>` where `<target>` is a Ruby
|
|
42
|
-
# identifier (parameter name) or `self`, and `<ClassName>`
|
|
43
|
-
# is a single non-namespaced class identifier or a
|
|
44
|
-
# `::Foo::Bar` style constant path. Negative refinements
|
|
45
|
-
# (`~T`), intersections, and unions are deferred to the
|
|
46
|
-
# next iteration.
|
|
26
|
+
# `predicate-if-*` fires when the call is used as an `if` / `unless`
|
|
27
|
+
# condition; `assert` fires unconditionally at the call's post-scope;
|
|
28
|
+
# `assert-if-true` / `assert-if-false` fire at the post-scope only
|
|
29
|
+
# when the call's return value can be observed as truthy / falsey.
|
|
30
|
+
# Negation (`~T`) is supported for both class-name and refinement
|
|
31
|
+
# right-hand sides. Parameterised refinements (`non-empty-array[T]`)
|
|
32
|
+
# are also recognised. Annotations whose directive is unrecognised
|
|
33
|
+
# are silently ignored per the spec's "unsupported metadata" guidance.
|
|
47
34
|
module RbsExtended # rubocop:disable Metrics/ModuleLength
|
|
48
35
|
DIRECTIVE_PREFIX = "rigor:v1:"
|
|
49
36
|
|
|
@@ -58,9 +45,10 @@ module Rigor
|
|
|
58
45
|
# a kebab-case refinement name (`non-empty-string`,
|
|
59
46
|
# `lowercase-string`, …) instead of a Capitalised class
|
|
60
47
|
# name. The narrowing tier substitutes the carrier for the
|
|
61
|
-
# current local type; `class_name` is then nil
|
|
62
|
-
#
|
|
63
|
-
#
|
|
48
|
+
# current local type; `class_name` is then nil. `negative`
|
|
49
|
+
# may be true for refinement-form directives — `~T` negation
|
|
50
|
+
# is supported; the narrowing tier computes the complement
|
|
51
|
+
# decomposition (see `AssertEffect` docs below).
|
|
64
52
|
class PredicateEffect < Data.define(:edge, :target_kind, :target_name, :class_name, :negative, :refinement_type)
|
|
65
53
|
def truthy_only? = edge == :truthy_only
|
|
66
54
|
def falsey_only? = edge == :falsey_only
|
data/lib/rigor/reflection.rb
CHANGED
|
@@ -17,11 +17,8 @@ module Rigor
|
|
|
17
17
|
# classes / modules, in-source constants, discovered method
|
|
18
18
|
# nodes, class ivar / cvar declarations).
|
|
19
19
|
#
|
|
20
|
-
# This module is the **stable read shape**
|
|
21
|
-
#
|
|
22
|
-
# calls out a unified reflection layer as a prerequisite for the
|
|
23
|
-
# extension protocols, and `docs/design/20260505-v0.1.0-readiness.md`
|
|
24
|
-
# nominates this module as the highest-leverage cold-start slice.
|
|
20
|
+
# This module is the **stable read shape** the plugin API is
|
|
21
|
+
# designed against (ADR-2, `docs/adr/2-extension-api.md`).
|
|
25
22
|
#
|
|
26
23
|
# The facade is **read-only and additive**. Existing call sites
|
|
27
24
|
# that read directly from `Rigor::Scope` or
|
|
@@ -52,8 +49,8 @@ module Rigor
|
|
|
52
49
|
# defined in the analyzed sources?
|
|
53
50
|
#
|
|
54
51
|
# The provenance side of the API (which source family contributed
|
|
55
|
-
# each fact) is explicitly out of scope for the v0.0.7 first
|
|
56
|
-
#
|
|
52
|
+
# each fact) is explicitly out of scope for the v0.0.7 first pass;
|
|
53
|
+
# v0.1.0's plugin API added it as a separate concern.
|
|
57
54
|
module Reflection
|
|
58
55
|
module_function
|
|
59
56
|
|
|
@@ -28,7 +28,9 @@ module Rigor
|
|
|
28
28
|
:discovered_superclasses,
|
|
29
29
|
:discovered_includes,
|
|
30
30
|
:discovered_class_sources,
|
|
31
|
-
:data_member_layouts
|
|
31
|
+
:data_member_layouts,
|
|
32
|
+
:struct_member_layouts,
|
|
33
|
+
:param_inferred_types
|
|
32
34
|
)
|
|
33
35
|
|
|
34
36
|
class DiscoveryIndex
|
|
@@ -53,7 +55,17 @@ module Rigor
|
|
|
53
55
|
discovered_superclasses: EMPTY_TABLE,
|
|
54
56
|
discovered_includes: EMPTY_TABLE,
|
|
55
57
|
discovered_class_sources: EMPTY_TABLE,
|
|
56
|
-
data_member_layouts: EMPTY_TABLE
|
|
58
|
+
data_member_layouts: EMPTY_TABLE,
|
|
59
|
+
struct_member_layouts: EMPTY_TABLE,
|
|
60
|
+
# ADR-67 WD3 — the call-site parameter-inference table, keyed by
|
|
61
|
+
# `[class_name, method_name, kind]` (the same `(class, method, kind)`
|
|
62
|
+
# triple {Inference::ParameterInferenceCollector} records and that
|
|
63
|
+
# `build_method_entry_scope` reconstructs from the lexical class path).
|
|
64
|
+
# The value is a `{param_name(Symbol) => Rigor::Type}` map of the union
|
|
65
|
+
# of resolved call-site argument types. Empty on every normal run; only
|
|
66
|
+
# the `coverage --protection` collection pass populates it today, so a
|
|
67
|
+
# `check` run leaves it empty and seeds nothing (byte-identical).
|
|
68
|
+
param_inferred_types: EMPTY_TABLE
|
|
57
69
|
)
|
|
58
70
|
end
|
|
59
71
|
end
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Rigor
|
|
|
23
23
|
:ivars, :cvars, :globals,
|
|
24
24
|
:indexed_narrowings, :method_chain_narrowings,
|
|
25
25
|
:declaration_sourced,
|
|
26
|
-
:source_path, :discovery
|
|
26
|
+
:source_path, :discovery, :struct_fold_safe_locals
|
|
27
27
|
|
|
28
28
|
# ADR-53 Track A — the seed-time discovery tables live on the
|
|
29
29
|
# {DiscoveryIndex} the scope carries by a single reference; the
|
|
@@ -51,6 +51,13 @@ module Rigor
|
|
|
51
51
|
def discovered_includes = @discovery.discovered_includes
|
|
52
52
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
53
53
|
def data_member_layouts = @discovery.data_member_layouts
|
|
54
|
+
def struct_member_layouts = @discovery.struct_member_layouts
|
|
55
|
+
# ADR-67 WD3 — call-site-inferred parameter types, keyed by
|
|
56
|
+
# `[class_name, method_name, kind]`. `build_method_entry_scope` consults
|
|
57
|
+
# this to seed an undeclared `def` parameter with the union of its
|
|
58
|
+
# resolved call-site argument types (precision-additive; an RBS-declared
|
|
59
|
+
# parameter always wins). Empty unless a collection pass seeded it.
|
|
60
|
+
def param_inferred_types = @discovery.param_inferred_types
|
|
54
61
|
|
|
55
62
|
# Narrowing key for an indexed read `receiver[key]` where both
|
|
56
63
|
# the receiver and the key are stable enough to address. The
|
|
@@ -99,8 +106,14 @@ module Rigor
|
|
|
99
106
|
# principle. Any flow-live touch (write / narrowing) drops the mark, so
|
|
100
107
|
# the diagnostic keeps firing exactly as before on flow-observed nil.
|
|
101
108
|
EMPTY_DECLARATION_SOURCED = Set.new.freeze
|
|
109
|
+
# ADR-48 Struct slice 3 — the per-body set of local names whose struct
|
|
110
|
+
# member reads are fold-safe (provably never mutated / aliased / escaped).
|
|
111
|
+
# A static per-scope context like {#source_path}: inherited unchanged
|
|
112
|
+
# through flow transitions and ignored by `==` / `hash`.
|
|
113
|
+
EMPTY_FOLD_SAFE = Set.new.freeze
|
|
102
114
|
private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
|
|
103
|
-
:EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED
|
|
115
|
+
:EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
|
|
116
|
+
:EMPTY_FOLD_SAFE
|
|
104
117
|
|
|
105
118
|
class << self
|
|
106
119
|
def empty(environment: Environment.default, source_path: nil)
|
|
@@ -120,7 +133,8 @@ module Rigor
|
|
|
120
133
|
indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
|
|
121
134
|
method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
|
|
122
135
|
declaration_sourced: EMPTY_DECLARATION_SOURCED,
|
|
123
|
-
source_path: nil
|
|
136
|
+
source_path: nil,
|
|
137
|
+
struct_fold_safe_locals: EMPTY_FOLD_SAFE
|
|
124
138
|
)
|
|
125
139
|
@environment = environment
|
|
126
140
|
@locals = locals
|
|
@@ -134,6 +148,7 @@ module Rigor
|
|
|
134
148
|
@method_chain_narrowings = method_chain_narrowings
|
|
135
149
|
@declaration_sourced = declaration_sourced
|
|
136
150
|
@source_path = source_path
|
|
151
|
+
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
137
152
|
freeze
|
|
138
153
|
end
|
|
139
154
|
|
|
@@ -180,17 +195,29 @@ module Rigor
|
|
|
180
195
|
rebuild(self_type: type)
|
|
181
196
|
end
|
|
182
197
|
|
|
183
|
-
# ADR-
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
# file does this call site belong to?" without
|
|
198
|
+
# ADR-28 / ADR-52 slice 5a — per-file source path carried on
|
|
199
|
+
# the scope. The analyzer stamps the current file's path onto
|
|
200
|
+
# the seed scope; nested rebuilds propagate it so plugin rules
|
|
201
|
+
# (`dynamic_return`'s `file_methods:` gate, sigil checks) can
|
|
202
|
+
# resolve "which file does this call site belong to?" without
|
|
189
203
|
# thread-locals.
|
|
190
204
|
def with_source_path(path)
|
|
191
205
|
rebuild(source_path: path)
|
|
192
206
|
end
|
|
193
207
|
|
|
208
|
+
# ADR-48 Struct slice 3 — installs the per-body fold-safe-local set
|
|
209
|
+
# ({Inference::StructFoldSafety}). Set once at body entry; inherited
|
|
210
|
+
# unchanged through subsequent flow transitions.
|
|
211
|
+
def with_struct_fold_safe(locals)
|
|
212
|
+
rebuild(struct_fold_safe_locals: locals)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# True when `name`'s `Struct` member reads are fold-safe in this body
|
|
216
|
+
# (the local is provably never mutated / aliased / escaped).
|
|
217
|
+
def struct_fold_safe?(name)
|
|
218
|
+
@struct_fold_safe_locals.include?(name.to_sym)
|
|
219
|
+
end
|
|
220
|
+
|
|
194
221
|
# ADR-53 Track A — swaps the whole discovery index in one transition.
|
|
195
222
|
# The sole seeding path; the per-table writers it replaced are derived
|
|
196
223
|
# off-`Scope` through `scope.discovery.with(table_name: table)`.
|
|
@@ -484,6 +511,20 @@ module Rigor
|
|
|
484
511
|
layout
|
|
485
512
|
end
|
|
486
513
|
|
|
514
|
+
# ADR-48 Struct follow-up — the `{ members:, keyword_init: }` layout
|
|
515
|
+
# recorded for a `Struct.new(...)`-defined class, in the constant form
|
|
516
|
+
# (`Point = Struct.new(:x, :y)`) and the named-subclass form
|
|
517
|
+
# (`class Point < Struct.new(:x, :y)`). Consumed by
|
|
518
|
+
# {Inference::MethodDispatcher::StructFolding} so `Point.new(...)` on a
|
|
519
|
+
# `Singleton[Point]` receiver materialises a member instance. Returns nil
|
|
520
|
+
# when the class has no recorded struct layout. Mirrors
|
|
521
|
+
# {#data_member_layout}'s dependency-recording contract.
|
|
522
|
+
def struct_member_layout(class_name)
|
|
523
|
+
layout = @discovery.struct_member_layouts[class_name.to_s]
|
|
524
|
+
record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
|
|
525
|
+
layout
|
|
526
|
+
end
|
|
527
|
+
|
|
487
528
|
# ADR-24 slice 2 — per-class/module table mapping a fully
|
|
488
529
|
# qualified user class or module to the list of module
|
|
489
530
|
# names it `include`s / `prepend`s, AS WRITTEN at the
|
|
@@ -674,7 +715,8 @@ module Rigor
|
|
|
674
715
|
indexed_narrowings: @indexed_narrowings,
|
|
675
716
|
method_chain_narrowings: @method_chain_narrowings,
|
|
676
717
|
declaration_sourced: @declaration_sourced,
|
|
677
|
-
source_path: @source_path
|
|
718
|
+
source_path: @source_path,
|
|
719
|
+
struct_fold_safe_locals: @struct_fold_safe_locals
|
|
678
720
|
)
|
|
679
721
|
self.class.new(
|
|
680
722
|
environment: environment, locals: locals,
|
|
@@ -684,7 +726,8 @@ module Rigor
|
|
|
684
726
|
indexed_narrowings: indexed_narrowings,
|
|
685
727
|
method_chain_narrowings: method_chain_narrowings,
|
|
686
728
|
declaration_sourced: declaration_sourced,
|
|
687
|
-
source_path: source_path
|
|
729
|
+
source_path: source_path,
|
|
730
|
+
struct_fold_safe_locals: struct_fold_safe_locals
|
|
688
731
|
)
|
|
689
732
|
end
|
|
690
733
|
|
|
@@ -5,9 +5,9 @@ module Rigor
|
|
|
5
5
|
# Per-call-site argument observation produced by
|
|
6
6
|
# {ObservationCollector}. ADR-14 follow-up: the earlier
|
|
7
7
|
# MVP shape (`Array[Type]` of positional types only)
|
|
8
|
-
# could not represent keyword arguments —
|
|
9
|
-
# `MethodCatalog.new(path: ..., mutating_selectors: ...)`
|
|
10
|
-
#
|
|
8
|
+
# could not represent keyword arguments — keyword calls
|
|
9
|
+
# like `MethodCatalog.new(path: ..., mutating_selectors: ...)`
|
|
10
|
+
# were silently skipped in that shape.
|
|
11
11
|
# The new shape carries positional and keyword arg types
|
|
12
12
|
# in parallel so the per-position / per-keyword unions
|
|
13
13
|
# can each be reconstructed independently.
|