rigortype 0.2.0 → 0.2.2
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 +82 -20
- 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/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +557 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +532 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +26 -3
- data/lib/rigor/cli/coverage_command.rb +67 -92
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/docs_command.rb +248 -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/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration.rb +12 -0
- data/lib/rigor/environment/rbs_loader.rb +27 -0
- data/lib/rigor/environment.rb +49 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/inference/statement_evaluator.rb +27 -0
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +98 -38
- data/lib/rigor/protection/mutator.rb +21 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- metadata +120 -1
|
@@ -88,6 +88,7 @@ module Rigor
|
|
|
88
88
|
to_h: :tuple_to_h,
|
|
89
89
|
zip: :tuple_zip,
|
|
90
90
|
:[] => :tuple_index,
|
|
91
|
+
slice: :tuple_index,
|
|
91
92
|
fetch: :tuple_index,
|
|
92
93
|
dig: :tuple_dig,
|
|
93
94
|
values_at: :tuple_values_at,
|
|
@@ -842,7 +843,10 @@ module Rigor
|
|
|
842
843
|
|
|
843
844
|
# `tuple.min` / `tuple.max` — fold when every element is
|
|
844
845
|
# a `Constant` whose values share a Ruby-comparable
|
|
845
|
-
# domain. Empty tuples fold to `Constant[nil]`.
|
|
846
|
+
# domain. Empty tuples fold to `Constant[nil]`. The 1-arg
|
|
847
|
+
# `min(n)` / `max(n)` form folds to a `Tuple` of the n
|
|
848
|
+
# edge-most values in Ruby's order (`min(n)` ascending,
|
|
849
|
+
# `max(n)` descending) — the n-arg sibling of `first(n)`.
|
|
846
850
|
def tuple_min(tuple, _method_name, args)
|
|
847
851
|
tuple_minmax(tuple, args, :min)
|
|
848
852
|
end
|
|
@@ -852,7 +856,7 @@ module Rigor
|
|
|
852
856
|
end
|
|
853
857
|
|
|
854
858
|
def tuple_minmax(tuple, args, edge)
|
|
855
|
-
return
|
|
859
|
+
return tuple_minmax_n(tuple, args.first, edge) unless args.empty?
|
|
856
860
|
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
857
861
|
|
|
858
862
|
values = constant_values(tuple.elements)
|
|
@@ -864,6 +868,25 @@ module Rigor
|
|
|
864
868
|
nil
|
|
865
869
|
end
|
|
866
870
|
|
|
871
|
+
# `tuple.min(n)` / `tuple.max(n)` — a `Tuple` of the n
|
|
872
|
+
# edge-most element values, delegating to Ruby's
|
|
873
|
+
# `Array#min` / `#max` for the ordering. Declines on a
|
|
874
|
+
# non-static / negative count or non-Constant elements.
|
|
875
|
+
# The result is bounded by the tuple's known arity, so no
|
|
876
|
+
# extra size cap is needed.
|
|
877
|
+
def tuple_minmax_n(tuple, arg, edge)
|
|
878
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
|
|
879
|
+
return nil if arg.value.negative?
|
|
880
|
+
|
|
881
|
+
values = constant_values(tuple.elements)
|
|
882
|
+
return nil if values.nil?
|
|
883
|
+
|
|
884
|
+
picked = values.public_send(edge, arg.value)
|
|
885
|
+
Type::Combinator.tuple_of(*picked.map { |v| Type::Combinator.constant_of(v) })
|
|
886
|
+
rescue StandardError
|
|
887
|
+
nil
|
|
888
|
+
end
|
|
889
|
+
|
|
867
890
|
# `tuple.minmax` — the `[min, max]` pair as a 2-slot
|
|
868
891
|
# `Tuple[Constant[min], Constant[max]]`, mirroring the
|
|
869
892
|
# `Range#minmax` fold. Every element must be a `Constant`
|
|
@@ -1201,6 +1224,13 @@ module Rigor
|
|
|
1201
1224
|
# indices still fall through because the same handler serves
|
|
1202
1225
|
# `fetch`, while statically nil slices can be represented
|
|
1203
1226
|
# precisely for `[]`.
|
|
1227
|
+
# `[]` and its exact alias `slice` share the index / Range /
|
|
1228
|
+
# start-length folding. `fetch` routes here too but stays
|
|
1229
|
+
# integer-index-only: the Range and start-length branches gate
|
|
1230
|
+
# on this selector set, which `fetch` is deliberately not in.
|
|
1231
|
+
SLICE_SELECTORS = Set[:[], :slice].freeze
|
|
1232
|
+
private_constant :SLICE_SELECTORS
|
|
1233
|
+
|
|
1204
1234
|
def tuple_index(tuple, method_name, args)
|
|
1205
1235
|
case args.size
|
|
1206
1236
|
when 1 then tuple_single_index(tuple, method_name, args.first)
|
|
@@ -1211,17 +1241,18 @@ module Rigor
|
|
|
1211
1241
|
def tuple_single_index(tuple, method_name, arg)
|
|
1212
1242
|
return nil unless arg.is_a?(Type::Constant)
|
|
1213
1243
|
|
|
1214
|
-
|
|
1215
|
-
return
|
|
1244
|
+
value = arg.value
|
|
1245
|
+
return tuple_range_slice(tuple, value) if SLICE_SELECTORS.include?(method_name) && value.is_a?(Range)
|
|
1246
|
+
return nil unless value.is_a?(Integer)
|
|
1216
1247
|
|
|
1217
|
-
idx = normalise_index(
|
|
1248
|
+
idx = normalise_index(value, tuple.elements.size)
|
|
1218
1249
|
return nil unless idx
|
|
1219
1250
|
|
|
1220
1251
|
tuple.elements[idx]
|
|
1221
1252
|
end
|
|
1222
1253
|
|
|
1223
1254
|
def tuple_start_length_slice(tuple, method_name, args)
|
|
1224
|
-
return nil unless method_name
|
|
1255
|
+
return nil unless SLICE_SELECTORS.include?(method_name)
|
|
1225
1256
|
|
|
1226
1257
|
start, length = args
|
|
1227
1258
|
return nil unless start.is_a?(Type::Constant) && length.is_a?(Type::Constant)
|
|
@@ -118,10 +118,12 @@ module Rigor
|
|
|
118
118
|
# table to suppress false positives for methods the
|
|
119
119
|
# user has defined but no RBS sig describes. Merged
|
|
120
120
|
# UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
# One combined descent yields both the discovered-methods existence
|
|
122
|
+
# table and the instance def-node table — see
|
|
123
|
+
# {#build_methods_and_def_nodes}. `seed_discovered_methods` seeds the
|
|
124
|
+
# former onto the scope and returns the def-node table for
|
|
125
|
+
# `merge_project_method_indexes` below.
|
|
126
|
+
seeded_scope, file_def_nodes = seed_discovered_methods(seeded_scope, default_scope, root)
|
|
125
127
|
|
|
126
128
|
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
|
|
127
129
|
# def nodes, the class -> superclass map, and the
|
|
@@ -134,7 +136,7 @@ module Rigor
|
|
|
134
136
|
# table. Seeded inside `merge_project_method_indexes` so the
|
|
135
137
|
# per-file visibilities merge OVER the cross-file project seed
|
|
136
138
|
# rather than overwriting it.
|
|
137
|
-
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root)
|
|
139
|
+
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
|
|
138
140
|
|
|
139
141
|
table = {}.compare_by_identity
|
|
140
142
|
table.default = seeded_scope
|
|
@@ -160,6 +162,19 @@ module Rigor
|
|
|
160
162
|
table
|
|
161
163
|
end
|
|
162
164
|
|
|
165
|
+
# Runs the combined methods/def-nodes descent (one walk of the file),
|
|
166
|
+
# seeds the discovered-methods existence table onto `seeded_scope`
|
|
167
|
+
# (merged UNDER the cross-file pre-pass seed `default_scope` carries),
|
|
168
|
+
# and returns `[scope, file_def_nodes]` so the caller can thread the
|
|
169
|
+
# def-node table into {#merge_project_method_indexes} without walking
|
|
170
|
+
# the file a second time.
|
|
171
|
+
def seed_discovered_methods(seeded_scope, default_scope, root)
|
|
172
|
+
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
173
|
+
discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
|
|
174
|
+
scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
|
|
175
|
+
[scope, file_def_nodes]
|
|
176
|
+
end
|
|
177
|
+
|
|
163
178
|
# ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
|
|
164
179
|
# ({Inference::StructFoldSafety}). Struct member layouts of constant
|
|
165
180
|
# receivers are resolved through the side-table the seeded scope carries.
|
|
@@ -179,9 +194,9 @@ module Rigor
|
|
|
179
194
|
# `discovered_def_index_for_paths` seed carried on
|
|
180
195
|
# `default_scope` — same-file declarations win per entry,
|
|
181
196
|
# the cross-file seed supplies sibling-file ancestors.
|
|
182
|
-
def merge_project_method_indexes(seeded_scope, default_scope, root)
|
|
197
|
+
def merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
|
|
183
198
|
def_nodes = default_scope.discovered_def_nodes.merge(
|
|
184
|
-
|
|
199
|
+
file_def_nodes
|
|
185
200
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
186
201
|
singleton_def_nodes = default_scope.discovered_singleton_def_nodes.merge(
|
|
187
202
|
build_discovered_singleton_def_nodes(root)
|
|
@@ -1406,16 +1421,29 @@ module Rigor
|
|
|
1406
1421
|
Type::Combinator.singleton_of(full)
|
|
1407
1422
|
end
|
|
1408
1423
|
|
|
1409
|
-
# Slice 7 phase 12 — in-source method discovery pre-pass
|
|
1410
|
-
#
|
|
1411
|
-
#
|
|
1412
|
-
#
|
|
1413
|
-
#
|
|
1414
|
-
# `
|
|
1415
|
-
def
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1424
|
+
# Slice 7 phase 12 — in-source method discovery pre-pass, fused with
|
|
1425
|
+
# the instance-method def-node pre-pass (v0.0.2 #5). One descent
|
|
1426
|
+
# produces BOTH tables the per-file `index` and the cross-file
|
|
1427
|
+
# pre-pass each need together:
|
|
1428
|
+
#
|
|
1429
|
+
# - `methods` : `{class_name => {method => :instance | :singleton}}`
|
|
1430
|
+
# for every `def` / `define_method(:name)` / `attr_*` / `alias` /
|
|
1431
|
+
# Data/Struct-member reader (the undefined-method existence table).
|
|
1432
|
+
# - `def_nodes` : `{class_name => {method => Prism::DefNode}}` for
|
|
1433
|
+
# every instance-side `def` (the inter-procedural return-inference
|
|
1434
|
+
# table; singleton defs and `define_method` are intentionally
|
|
1435
|
+
# skipped — `record_def_node` filters them).
|
|
1436
|
+
#
|
|
1437
|
+
# `walk_methods` and `walk_def_nodes` had byte-identical class /
|
|
1438
|
+
# module / singleton / meta-block descents (both stop at `DefNode`),
|
|
1439
|
+
# so a single combined walk records both accumulators at once instead
|
|
1440
|
+
# of traversing every file twice.
|
|
1441
|
+
def build_methods_and_def_nodes(root)
|
|
1442
|
+
methods = {}
|
|
1443
|
+
def_nodes = {}
|
|
1444
|
+
walk_methods_and_def_nodes(root, [], false, methods, def_nodes)
|
|
1445
|
+
apply_alias_def_nodes(root, def_nodes)
|
|
1446
|
+
[methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
|
|
1419
1447
|
end
|
|
1420
1448
|
|
|
1421
1449
|
# Merges two `class_name => { method => kind }` tables, unioning
|
|
@@ -1431,7 +1459,14 @@ module Rigor
|
|
|
1431
1459
|
end
|
|
1432
1460
|
|
|
1433
1461
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
1434
|
-
|
|
1462
|
+
# Combined `walk_methods` + `walk_def_nodes` descent. The two walks
|
|
1463
|
+
# had identical class / module / singleton-class / meta-block
|
|
1464
|
+
# traversals and both stopped at `DefNode`; the only divergences are
|
|
1465
|
+
# leaf actions (recorded into the right accumulator) and the original
|
|
1466
|
+
# `walk_methods` returning at `AliasMethodNode` (its symbol-only
|
|
1467
|
+
# children carry no def / class node, so not descending them is
|
|
1468
|
+
# byte-identical for `def_nodes` too). See {#build_methods_and_def_nodes}.
|
|
1469
|
+
def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
|
|
1435
1470
|
return unless node.is_a?(Prism::Node)
|
|
1436
1471
|
|
|
1437
1472
|
case node
|
|
@@ -1439,40 +1474,40 @@ module Rigor
|
|
|
1439
1474
|
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1440
1475
|
if name
|
|
1441
1476
|
child_prefix = qualified_prefix + [name]
|
|
1442
|
-
record_meta_superclass_members(node, child_prefix,
|
|
1443
|
-
|
|
1477
|
+
record_meta_superclass_members(node, child_prefix, methods_acc) if node.is_a?(Prism::ClassNode)
|
|
1478
|
+
walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc) if node.body
|
|
1444
1479
|
return
|
|
1445
1480
|
end
|
|
1446
1481
|
when Prism::SingletonClassNode
|
|
1447
1482
|
if node.body
|
|
1448
1483
|
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
1449
1484
|
if singleton_prefix
|
|
1450
|
-
|
|
1485
|
+
walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc)
|
|
1451
1486
|
return
|
|
1452
1487
|
end
|
|
1453
1488
|
end
|
|
1454
1489
|
when Prism::ConstantWriteNode
|
|
1455
1490
|
if meta_new_block_body(node)
|
|
1456
1491
|
child_prefix = qualified_prefix + [node.name.to_s]
|
|
1457
|
-
|
|
1492
|
+
walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc)
|
|
1458
1493
|
return
|
|
1459
1494
|
end
|
|
1460
1495
|
when Prism::DefNode
|
|
1461
|
-
record_def_method(node, qualified_prefix, in_singleton_class,
|
|
1496
|
+
record_def_method(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
1497
|
+
record_def_node(node, qualified_prefix, in_singleton_class, def_nodes_acc)
|
|
1462
1498
|
return
|
|
1463
1499
|
when Prism::AliasMethodNode
|
|
1464
|
-
record_alias_method(node, qualified_prefix, in_singleton_class,
|
|
1500
|
+
record_alias_method(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
1465
1501
|
return
|
|
1466
1502
|
when Prism::CallNode
|
|
1467
|
-
record_define_method(node, qualified_prefix, in_singleton_class,
|
|
1503
|
+
record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
|
|
1468
1504
|
if ATTR_MACROS.include?(node.name)
|
|
1469
|
-
record_attr_methods(node, qualified_prefix, in_singleton_class,
|
|
1470
|
-
accumulator)
|
|
1505
|
+
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
1471
1506
|
end
|
|
1472
1507
|
end
|
|
1473
1508
|
|
|
1474
1509
|
node.compact_child_nodes.each do |child|
|
|
1475
|
-
|
|
1510
|
+
walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
|
|
1476
1511
|
end
|
|
1477
1512
|
end
|
|
1478
1513
|
|
|
@@ -1606,57 +1641,6 @@ module Rigor
|
|
|
1606
1641
|
end
|
|
1607
1642
|
end
|
|
1608
1643
|
|
|
1609
|
-
# v0.0.2 #5 — instance-side def-node recording. Walks
|
|
1610
|
-
# class bodies the same way as `build_discovered_methods`
|
|
1611
|
-
# but records the actual `Prism::DefNode` for each
|
|
1612
|
-
# **instance** method so `ExpressionTyper` can re-type
|
|
1613
|
-
# the body at the call site for inter-procedural return
|
|
1614
|
-
# inference. Singleton methods and `define_method` calls
|
|
1615
|
-
# are intentionally skipped: the inference path needs a
|
|
1616
|
-
# statically introspectable body, and singleton dispatch
|
|
1617
|
-
# has its own complications (Class / Module ancestry)
|
|
1618
|
-
# the first-iteration rule does not yet model.
|
|
1619
|
-
def build_discovered_def_nodes(root)
|
|
1620
|
-
accumulator = {}
|
|
1621
|
-
walk_def_nodes(root, [], false, accumulator)
|
|
1622
|
-
apply_alias_def_nodes(root, accumulator)
|
|
1623
|
-
accumulator.transform_values(&:freeze).freeze
|
|
1624
|
-
end
|
|
1625
|
-
|
|
1626
|
-
def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
|
|
1627
|
-
return unless node.is_a?(Prism::Node)
|
|
1628
|
-
|
|
1629
|
-
case node
|
|
1630
|
-
when Prism::ClassNode, Prism::ModuleNode
|
|
1631
|
-
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1632
|
-
if name
|
|
1633
|
-
child_prefix = qualified_prefix + [name]
|
|
1634
|
-
walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
|
|
1635
|
-
return
|
|
1636
|
-
end
|
|
1637
|
-
when Prism::SingletonClassNode
|
|
1638
|
-
if node.body
|
|
1639
|
-
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
1640
|
-
if singleton_prefix
|
|
1641
|
-
walk_def_nodes(node.body, singleton_prefix, true, accumulator)
|
|
1642
|
-
return
|
|
1643
|
-
end
|
|
1644
|
-
end
|
|
1645
|
-
when Prism::ConstantWriteNode
|
|
1646
|
-
if meta_new_block_body(node)
|
|
1647
|
-
child_prefix = qualified_prefix + [node.name.to_s]
|
|
1648
|
-
walk_def_nodes(meta_new_block_body(node), child_prefix, false, accumulator)
|
|
1649
|
-
return
|
|
1650
|
-
end
|
|
1651
|
-
when Prism::DefNode
|
|
1652
|
-
record_def_node(node, qualified_prefix, in_singleton_class, accumulator)
|
|
1653
|
-
return
|
|
1654
|
-
end
|
|
1655
|
-
|
|
1656
|
-
node.compact_child_nodes.each do |child|
|
|
1657
|
-
walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
|
|
1658
|
-
end
|
|
1659
|
-
end
|
|
1660
1644
|
# v0.0.3 A — sentinel key under which `record_def_node`
|
|
1661
1645
|
# files DefNodes that live outside any class / module
|
|
1662
1646
|
# body (top-level helpers, `def`s nested inside DSL
|
|
@@ -2385,7 +2369,13 @@ module Rigor
|
|
|
2385
2369
|
# the override-visibility-reduced rule can read an ancestor's
|
|
2386
2370
|
# visibility declared in a sibling file.
|
|
2387
2371
|
def accumulate_project_index(acc, path, root)
|
|
2388
|
-
|
|
2372
|
+
# One combined descent yields both the methods existence table and
|
|
2373
|
+
# the def-node table; the latter is also consumed by
|
|
2374
|
+
# `record_class_sources`, so a def-dense file is walked once here
|
|
2375
|
+
# instead of three times (methods + def-nodes ×2). See
|
|
2376
|
+
# {#build_methods_and_def_nodes}.
|
|
2377
|
+
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
2378
|
+
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
2389
2379
|
build_discovered_singleton_def_nodes(root).each do |class_name, methods|
|
|
2390
2380
|
(acc[:singleton_def_nodes][class_name] ||= {}).merge!(methods)
|
|
2391
2381
|
end
|
|
@@ -2395,20 +2385,28 @@ module Rigor
|
|
|
2395
2385
|
includes.each do |class_name, mods|
|
|
2396
2386
|
acc[:includes][class_name] = ((acc[:includes][class_name] || []) + mods).uniq
|
|
2397
2387
|
end
|
|
2398
|
-
record_class_sources(acc[:class_sources], path, root, superclasses, includes)
|
|
2399
|
-
merge_class_keyed_index_tables(acc, root)
|
|
2388
|
+
record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes)
|
|
2389
|
+
merge_class_keyed_index_tables(acc, root, file_methods)
|
|
2390
|
+
merge_member_layout_tables(acc, root)
|
|
2391
|
+
end
|
|
2392
|
+
|
|
2393
|
+
# Folds one file's Data + Struct member-layout tables into the
|
|
2394
|
+
# cross-file accumulator (kept out of {#accumulate_project_index} to
|
|
2395
|
+
# hold its ABC budget).
|
|
2396
|
+
def merge_member_layout_tables(acc, root)
|
|
2400
2397
|
acc[:data_member_layouts].merge!(build_data_member_layouts(root))
|
|
2401
2398
|
acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
|
|
2402
2399
|
end
|
|
2403
2400
|
|
|
2404
2401
|
# Folds the per-class method-visibility and method-existence tables of
|
|
2405
2402
|
# one file into the cross-file accumulator (kept out of
|
|
2406
|
-
# {#accumulate_project_index} to hold its ABC budget).
|
|
2407
|
-
def
|
|
2403
|
+
# {#accumulate_project_index} to hold its ABC budget). `file_methods`
|
|
2404
|
+
# is the existence table from the combined methods/def-nodes descent.
|
|
2405
|
+
def merge_class_keyed_index_tables(acc, root, file_methods)
|
|
2408
2406
|
build_discovered_method_visibilities(root).each do |class_name, table|
|
|
2409
2407
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
2410
2408
|
end
|
|
2411
|
-
|
|
2409
|
+
file_methods.each do |class_name, table|
|
|
2412
2410
|
(acc[:methods][class_name] ||= {}).merge!(table)
|
|
2413
2411
|
end
|
|
2414
2412
|
end
|
|
@@ -2423,13 +2421,13 @@ module Rigor
|
|
|
2423
2421
|
# dependency recording (ADR-46). The class-declaration walk
|
|
2424
2422
|
# (`collect_class_decls`) catches bodyless / def-less reopenings the
|
|
2425
2423
|
# other three builders miss.
|
|
2426
|
-
def record_class_sources(class_sources, path, root, superclasses, includes)
|
|
2424
|
+
def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes)
|
|
2427
2425
|
names = Set.new
|
|
2428
2426
|
collect_class_decls(root, [], decls = {})
|
|
2429
2427
|
names.merge(decls.keys)
|
|
2430
2428
|
names.merge(superclasses.keys)
|
|
2431
2429
|
names.merge(includes.keys)
|
|
2432
|
-
names.merge(
|
|
2430
|
+
names.merge(file_def_nodes.keys)
|
|
2433
2431
|
names.each { |name| (class_sources[name] ||= Set.new) << path }
|
|
2434
2432
|
end
|
|
2435
2433
|
|
|
@@ -2438,8 +2436,8 @@ module Rigor
|
|
|
2438
2436
|
# seen `"path:line"` definition site in `def_sources` (ADR-17 —
|
|
2439
2437
|
# the un-registered-project-patch signal `call.undefined-method`
|
|
2440
2438
|
# and `rigor triage` key on).
|
|
2441
|
-
def merge_discovered_defs(def_nodes, def_sources, path,
|
|
2442
|
-
|
|
2439
|
+
def merge_discovered_defs(def_nodes, def_sources, path, file_def_nodes)
|
|
2440
|
+
file_def_nodes.each do |class_name, methods|
|
|
2443
2441
|
(def_nodes[class_name] ||= {}).merge!(methods)
|
|
2444
2442
|
sources = (def_sources[class_name] ||= {})
|
|
2445
2443
|
methods.each do |method_name, def_node|
|
|
@@ -2274,6 +2274,11 @@ module Rigor
|
|
|
2274
2274
|
body = block_node.body
|
|
2275
2275
|
return post_scope if body.nil?
|
|
2276
2276
|
|
|
2277
|
+
# Transitive case first: the body may content-mutate a captured local
|
|
2278
|
+
# through a self-call rather than a direct `local[k] = v` write, which
|
|
2279
|
+
# `collect_content_mutations` cannot see (see below).
|
|
2280
|
+
post_scope = floor_block_body_callee_escaped_args(body, post_scope)
|
|
2281
|
+
|
|
2277
2282
|
mutations = collect_content_mutations(body)
|
|
2278
2283
|
return post_scope if mutations.empty?
|
|
2279
2284
|
|
|
@@ -2283,6 +2288,28 @@ module Rigor
|
|
|
2283
2288
|
end
|
|
2284
2289
|
end
|
|
2285
2290
|
|
|
2291
|
+
# Inside an ESCAPING block body, a captured outer local can be content-
|
|
2292
|
+
# mutated transitively: the body is (or contains) a self-call that
|
|
2293
|
+
# escape-mutates one of its arguments. The canonical shape is the CLI's
|
|
2294
|
+
# own `OptionParser.new { |opts| define_options(opts, options) }` — the
|
|
2295
|
+
# block body is a bare `define_options(opts, options)` whose `options`
|
|
2296
|
+
# parameter is escape-mutated inside ITS nested `opts.on { options[:k] =
|
|
2297
|
+
# v }` blocks. `collect_content_mutations` only sees direct `local[k] =
|
|
2298
|
+
# v` writes in THIS body, so it misses the transitive write and the
|
|
2299
|
+
# captured Hash keeps its literal-false seed (folding the caller's
|
|
2300
|
+
# `options[:mutation]` guard to an always-falsey constant). Reuse the
|
|
2301
|
+
# cross-method-boundary callee-escaped-argument floor (the same gate the
|
|
2302
|
+
# receiver-chain path uses at the call site) on every self-call in the
|
|
2303
|
+
# body. Sound — only ever floors a captured local passed as an argument
|
|
2304
|
+
# to a callee that demonstrably escape-mutates the matching parameter.
|
|
2305
|
+
def floor_block_body_callee_escaped_args(body, post_scope)
|
|
2306
|
+
acc = post_scope
|
|
2307
|
+
Source::NodeWalker.each(body) do |descendant|
|
|
2308
|
+
acc = floor_callee_escaped_args_for_call(descendant, acc) if descendant.is_a?(Prism::CallNode)
|
|
2309
|
+
end
|
|
2310
|
+
acc
|
|
2311
|
+
end
|
|
2312
|
+
|
|
2286
2313
|
# The Dynamic-floor carrier for a content-mutated escaping capture, or
|
|
2287
2314
|
# nil when the pre-state is not a recognised mutable collection (leave
|
|
2288
2315
|
# it alone — e.g. an already-`Dynamic` binding or an unknown shape).
|
|
@@ -12,13 +12,13 @@ module Rigor
|
|
|
12
12
|
# `exe/rigor` launcher maps `.rigor.yml`'s `plugins_isolation:` onto it
|
|
13
13
|
# before re-exec). Three backends behind one interface:
|
|
14
14
|
#
|
|
15
|
-
# - `none`
|
|
16
|
-
# Lowest cost; no isolation.
|
|
17
|
-
# invoked library is trusted + pure.
|
|
15
|
+
# - `none` — load into the main space and call directly.
|
|
16
|
+
# Lowest cost; no isolation. Used as the fallback where fork is
|
|
17
|
+
# unavailable; fine because the invoked library is trusted + pure.
|
|
18
18
|
# - `ruby_box` — call inside a {Box} (`Ruby::Box`, `RUBY_BOX=1`). Isolates
|
|
19
19
|
# core-class monkey-patches + lets gem versions coexist, but a native
|
|
20
20
|
# crash in the boxed work still takes the process down (in-process).
|
|
21
|
-
# - `process` — call in a forked worker ({Process}); returns data over a
|
|
21
|
+
# - `process` (**default**) — call in a forked worker ({Process}); returns data over a
|
|
22
22
|
# pipe. The strongest: a child crash (even `SIGSEGV`) is contained —
|
|
23
23
|
# the parent survives and declines. Higher cost (fork + IPC).
|
|
24
24
|
#
|
|
@@ -73,7 +73,7 @@ module Rigor
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# `none` — load the trusted library into the main space and call it
|
|
76
|
-
# directly. No isolation; lowest cost; the
|
|
76
|
+
# directly. No isolation; lowest cost; the fork-unavailable fallback.
|
|
77
77
|
module Direct
|
|
78
78
|
module_function
|
|
79
79
|
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -183,8 +183,10 @@ module Rigor
|
|
|
183
183
|
Plugin.registered_for(newly_registered.first)
|
|
184
184
|
else
|
|
185
185
|
raise LoadError.new(
|
|
186
|
-
"plugin gem #{entry[:gem].inspect}
|
|
187
|
-
"(#{newly_registered.sort.inspect})
|
|
186
|
+
"plugin gem #{entry[:gem].inspect} bundles #{newly_registered.size} plugins " \
|
|
187
|
+
"(#{newly_registered.sort.inspect}) and cannot be activated as a single `plugins:` entry — " \
|
|
188
|
+
"it is a convenience meta-gem. List the individual plugin gems you want in `plugins:` " \
|
|
189
|
+
"(e.g. `rigor-#{newly_registered.min}`), or select one with an explicit `id:` field.",
|
|
188
190
|
plugin_ref: entry[:gem]
|
|
189
191
|
)
|
|
190
192
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analysis/runner"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Protection
|
|
7
|
+
# ADR-69 Seam 1 — the **kill oracle** Rigor's analyzer-teeth measurement uses:
|
|
8
|
+
# a mutant is killed iff re-analysing it introduces a diagnostic absent from
|
|
9
|
+
# the clean baseline. This is exactly the behaviour ADR-62/63 shipped, lifted
|
|
10
|
+
# out of {MutationScanner} so a {TestSuiteOracle} (ADR-70) — which kills by
|
|
11
|
+
# *running tests* rather than by re-analysis — can sit beside it without the
|
|
12
|
+
# scanner baking in either assumption.
|
|
13
|
+
#
|
|
14
|
+
# The expensive builds (RBS environment + the whole-project pre-pass scan) are
|
|
15
|
+
# paid once by the caller and threaded in; each mutant reuses them through
|
|
16
|
+
# `Runner.new(prebuilt:)#run_source` (in-memory overlay, no disk write).
|
|
17
|
+
# Passing `prebuilt:` disables the run-result cache (whose key digests the
|
|
18
|
+
# *disk* file), so a mutant is never served a stale clean hit.
|
|
19
|
+
class DiagnosticOracle
|
|
20
|
+
def initialize(configuration:, environment:, project_scan:)
|
|
21
|
+
@configuration = configuration
|
|
22
|
+
@environment = environment
|
|
23
|
+
@project_scan = project_scan
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The clean per-file baseline: the diagnostic signatures a mutant must add
|
|
27
|
+
# to count as killed. Computed once per file by the caller.
|
|
28
|
+
def baseline(source:, path:)
|
|
29
|
+
analyse(source, path).to_set { |d| sig(d) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Killed iff the mutant introduces a diagnostic not in `baseline`.
|
|
33
|
+
def killed?(mutant_source:, path:, baseline:)
|
|
34
|
+
analyse(mutant_source, path).any? { |d| !baseline.include?(sig(d)) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def analyse(source, path)
|
|
40
|
+
Rigor::Analysis::Runner.new(
|
|
41
|
+
configuration: @configuration, environment: @environment, prebuilt: @project_scan,
|
|
42
|
+
cache_store: nil, collect_stats: false
|
|
43
|
+
).run_source(source: source, path: path).diagnostics
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sig(diagnostic)
|
|
47
|
+
[diagnostic.rule, diagnostic.path, diagnostic.line, diagnostic.column, diagnostic.message]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|