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
|
@@ -32,9 +32,9 @@ module Rigor
|
|
|
32
32
|
# its `#prepare(services)` hook. `consumes:` lists the
|
|
33
33
|
# `(plugin_id, name)` pairs this plugin reads from
|
|
34
34
|
# `services.fact_store`. The loader uses both for
|
|
35
|
-
# topological sort + missing-producer detection
|
|
36
|
-
#
|
|
37
|
-
#
|
|
35
|
+
# topological sort + missing-producer detection; slice 4
|
|
36
|
+
# added the declarations; slice 5 (`Loader#topo_sort_plugins`)
|
|
37
|
+
# enforces ordering and missing-producer validation.
|
|
38
38
|
class Consumption < Data.define(:plugin_id, :name, :optional)
|
|
39
39
|
def initialize(plugin_id:, name:, optional: false)
|
|
40
40
|
super(plugin_id: plugin_id.to_s, name: name.to_sym, optional: optional ? true : false)
|
|
@@ -277,10 +277,18 @@ module Rigor
|
|
|
277
277
|
end
|
|
278
278
|
end
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
# Shared shape check for the Array-of-X manifest fields. Every entry
|
|
281
|
+
# must satisfy the block; otherwise raise the uniform "must be an
|
|
282
|
+
# Array of <label>" message. Centralises the message format the
|
|
283
|
+
# field validators below share so it cannot drift between them.
|
|
284
|
+
def validate_array_of!(field, value, label, &)
|
|
285
|
+
return if value.is_a?(Array) && value.all?(&)
|
|
282
286
|
|
|
283
|
-
raise ArgumentError, "plugin manifest
|
|
287
|
+
raise ArgumentError, "plugin manifest #{field} must be an Array of #{label}, got #{value.inspect}"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def validate_produces!(produces)
|
|
291
|
+
validate_array_of!("produces", produces, "Symbol/String") { |p| p.is_a?(Symbol) || p.is_a?(String) }
|
|
284
292
|
end
|
|
285
293
|
|
|
286
294
|
# ADR-10 5a — `owns_receivers:` declares the class names
|
|
@@ -291,11 +299,7 @@ module Rigor
|
|
|
291
299
|
# so plugin contributions stay authoritative for those
|
|
292
300
|
# types.
|
|
293
301
|
def validate_owns_receivers!(owns_receivers)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
raise ArgumentError,
|
|
297
|
-
"plugin manifest owns_receivers must be an Array of non-empty String, " \
|
|
298
|
-
"got #{owns_receivers.inspect}"
|
|
302
|
+
validate_array_of!("owns_receivers", owns_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
|
|
299
303
|
end
|
|
300
304
|
|
|
301
305
|
# ADR-26 — `open_receivers:` declares the class names this
|
|
@@ -309,11 +313,7 @@ module Rigor
|
|
|
309
313
|
# `owns_receivers:` (which routes dispatch); this one only
|
|
310
314
|
# suppresses the diagnostic.
|
|
311
315
|
def validate_open_receivers!(open_receivers)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
raise ArgumentError,
|
|
315
|
-
"plugin manifest open_receivers must be an Array of non-empty String, " \
|
|
316
|
-
"got #{open_receivers.inspect}"
|
|
316
|
+
validate_array_of!("open_receivers", open_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
|
|
317
317
|
end
|
|
318
318
|
|
|
319
319
|
# ADR-13 slice 2 — `type_node_resolvers:` declares the
|
|
@@ -325,11 +325,9 @@ module Rigor
|
|
|
325
325
|
# integration that actually drives the chain lands in
|
|
326
326
|
# slice 3.
|
|
327
327
|
def validate_type_node_resolvers!(resolvers)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"plugin manifest type_node_resolvers must be an Array of " \
|
|
332
|
-
"Rigor::Plugin::TypeNodeResolver instances, got #{resolvers.inspect}"
|
|
328
|
+
validate_array_of!("type_node_resolvers", resolvers, "Rigor::Plugin::TypeNodeResolver instances") do |r|
|
|
329
|
+
r.is_a?(TypeNodeResolver)
|
|
330
|
+
end
|
|
333
331
|
end
|
|
334
332
|
|
|
335
333
|
# ADR-16 slice 1a — `block_as_methods:` declares the Tier A
|
|
@@ -338,11 +336,9 @@ module Rigor
|
|
|
338
336
|
# actually narrows `Scope#self_type` for matching blocks
|
|
339
337
|
# arrives in a subsequent slice.
|
|
340
338
|
def validate_block_as_methods!(entries)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
"plugin manifest block_as_methods must be an Array of " \
|
|
345
|
-
"Rigor::Plugin::Macro::BlockAsMethod instances, got #{entries.inspect}"
|
|
339
|
+
validate_array_of!("block_as_methods", entries, "Rigor::Plugin::Macro::BlockAsMethod instances") do |e|
|
|
340
|
+
e.is_a?(Macro::BlockAsMethod)
|
|
341
|
+
end
|
|
346
342
|
end
|
|
347
343
|
|
|
348
344
|
# ADR-16 slice 2a — `heredoc_templates:` declares the Tier C
|
|
@@ -351,11 +347,9 @@ module Rigor
|
|
|
351
347
|
# manifest; the pre-pass + `SyntheticMethodIndex` that actually
|
|
352
348
|
# emit synthetic methods arrive in slice 2b.
|
|
353
349
|
def validate_heredoc_templates!(entries)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
"plugin manifest heredoc_templates must be an Array of " \
|
|
358
|
-
"Rigor::Plugin::Macro::HeredocTemplate instances, got #{entries.inspect}"
|
|
350
|
+
validate_array_of!("heredoc_templates", entries, "Rigor::Plugin::Macro::HeredocTemplate instances") do |e|
|
|
351
|
+
e.is_a?(Macro::HeredocTemplate)
|
|
352
|
+
end
|
|
359
353
|
end
|
|
360
354
|
|
|
361
355
|
# ADR-36 — `nested_class_templates:` declares the
|
|
@@ -365,11 +359,10 @@ module Rigor
|
|
|
365
359
|
# subclasses + their `#inner` reader through the existing
|
|
366
360
|
# `SyntheticMethodIndex` primitive.
|
|
367
361
|
def validate_nested_class_templates!(entries)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"Rigor::Plugin::Macro::NestedClassTemplate instances, got #{entries.inspect}"
|
|
362
|
+
validate_array_of!("nested_class_templates", entries,
|
|
363
|
+
"Rigor::Plugin::Macro::NestedClassTemplate instances") do |e|
|
|
364
|
+
e.is_a?(Macro::NestedClassTemplate)
|
|
365
|
+
end
|
|
373
366
|
end
|
|
374
367
|
|
|
375
368
|
# ADR-16 slice 3a — `trait_registries:` declares the Tier B
|
|
@@ -379,11 +372,9 @@ module Rigor
|
|
|
379
372
|
# `SyntheticMethodIndex` (slice 2b primitive) arrives in
|
|
380
373
|
# slice 3b.
|
|
381
374
|
def validate_trait_registries!(entries)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
"plugin manifest trait_registries must be an Array of " \
|
|
386
|
-
"Rigor::Plugin::Macro::TraitRegistry instances, got #{entries.inspect}"
|
|
375
|
+
validate_array_of!("trait_registries", entries, "Rigor::Plugin::Macro::TraitRegistry instances") do |e|
|
|
376
|
+
e.is_a?(Macro::TraitRegistry)
|
|
377
|
+
end
|
|
387
378
|
end
|
|
388
379
|
|
|
389
380
|
# ADR-20 slice 6 — `hkt_registrations:` declares the
|
|
@@ -398,11 +389,9 @@ module Rigor
|
|
|
398
389
|
# user `.rbs` overlays merge on top of plugin entries
|
|
399
390
|
# last-write-wins.
|
|
400
391
|
def validate_hkt_registrations!(entries)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
"plugin manifest hkt_registrations must be an Array of " \
|
|
405
|
-
"Rigor::Inference::HktRegistry::Registration instances, got #{entries.inspect}"
|
|
392
|
+
validate_array_of!("hkt_registrations", entries, "Rigor::Inference::HktRegistry::Registration instances") do |e|
|
|
393
|
+
e.is_a?(Inference::HktRegistry::Registration)
|
|
394
|
+
end
|
|
406
395
|
end
|
|
407
396
|
|
|
408
397
|
# ADR-20 slice 6 — `hkt_definitions:` declares the
|
|
@@ -415,11 +404,9 @@ module Rigor
|
|
|
415
404
|
# via {Rigor::Inference::HktBody}'s node-constructor API
|
|
416
405
|
# without parsing a string.
|
|
417
406
|
def validate_hkt_definitions!(entries)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
"plugin manifest hkt_definitions must be an Array of " \
|
|
422
|
-
"Rigor::Inference::HktRegistry::Definition instances, got #{entries.inspect}"
|
|
407
|
+
validate_array_of!("hkt_definitions", entries, "Rigor::Inference::HktRegistry::Definition instances") do |e|
|
|
408
|
+
e.is_a?(Inference::HktRegistry::Definition)
|
|
409
|
+
end
|
|
423
410
|
end
|
|
424
411
|
|
|
425
412
|
# ADR-25 — `signature_paths:` declares the RBS signature
|
|
@@ -429,11 +416,7 @@ module Rigor
|
|
|
429
416
|
# loader validates each exists and `Environment.for_project`
|
|
430
417
|
# merges the resolved set into the RBS environment.
|
|
431
418
|
def validate_signature_paths!(paths)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
raise ArgumentError,
|
|
435
|
-
"plugin manifest signature_paths must be an Array of non-empty String, " \
|
|
436
|
-
"got #{paths.inspect}"
|
|
419
|
+
validate_array_of!("signature_paths", paths, "non-empty String") { |p| p.is_a?(String) && !p.empty? }
|
|
437
420
|
end
|
|
438
421
|
|
|
439
422
|
# ADR-28 — `protocol_contracts:` declares the path-scoped
|
|
@@ -449,11 +432,9 @@ module Rigor
|
|
|
449
432
|
# MAY override `Plugin::Base#protocol_contracts` to fold in
|
|
450
433
|
# per-project config (e.g. a custom convention path).
|
|
451
434
|
def validate_protocol_contracts!(entries)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
"plugin manifest protocol_contracts must be an Array of " \
|
|
456
|
-
"Rigor::Plugin::ProtocolContract instances, got #{entries.inspect}"
|
|
435
|
+
validate_array_of!("protocol_contracts", entries, "Rigor::Plugin::ProtocolContract instances") do |e|
|
|
436
|
+
e.is_a?(ProtocolContract)
|
|
437
|
+
end
|
|
457
438
|
end
|
|
458
439
|
|
|
459
440
|
# ADR-38 — `additional_initializers:` declares the
|
|
@@ -465,11 +446,9 @@ module Rigor
|
|
|
465
446
|
# loaded plugins; `Inference::ScopeIndexer` consults the set at
|
|
466
447
|
# its single gate.
|
|
467
448
|
def validate_additional_initializers!(entries)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
"plugin manifest additional_initializers must be an Array of " \
|
|
472
|
-
"Rigor::Plugin::AdditionalInitializer instances, got #{entries.inspect}"
|
|
449
|
+
validate_array_of!("additional_initializers", entries, "Rigor::Plugin::AdditionalInitializer instances") do |e|
|
|
450
|
+
e.is_a?(AdditionalInitializer)
|
|
451
|
+
end
|
|
473
452
|
end
|
|
474
453
|
|
|
475
454
|
# ADR-32 WD4 — `source_rbs_synthesizer:` declares a callable
|
|
@@ -317,13 +317,12 @@ module Rigor
|
|
|
317
317
|
!load_errors.empty?
|
|
318
318
|
end
|
|
319
319
|
|
|
320
|
-
# ADR-13
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
# — registration order is the user's lever.
|
|
320
|
+
# ADR-13 — flat ordered list of every loaded plugin's
|
|
321
|
+
# manifest-declared {TypeNodeResolver} instances, in plugin
|
|
322
|
+
# registration order. `Environment#build_name_scope` builds a
|
|
323
|
+
# `TypeNode::ResolverChain` from this list (environment.rb).
|
|
324
|
+
# The first non-nil `#resolve(node, scope)` return wins per
|
|
325
|
+
# ADR-13 WD3 / WD5 — registration order is the user's lever.
|
|
327
326
|
def type_node_resolvers
|
|
328
327
|
plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
|
|
329
328
|
end
|
|
@@ -24,14 +24,12 @@ module Rigor
|
|
|
24
24
|
# )
|
|
25
25
|
# end
|
|
26
26
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# in isolation but never run for a real `%a{rigor:v1:...}`
|
|
34
|
-
# payload.
|
|
27
|
+
# ADR-13 — base class, manifest hook, and registry aggregation
|
|
28
|
+
# for plugin-contributed type-node resolvers. Resolvers declared
|
|
29
|
+
# via `manifest(type_node_resolvers:)` run for every real
|
|
30
|
+
# `%a{rigor:v1:...}` payload through `TypeNode::ResolverChain`
|
|
31
|
+
# (built by `Environment#build_name_scope` from
|
|
32
|
+
# `Plugin::Registry#type_node_resolvers`).
|
|
35
33
|
#
|
|
36
34
|
# Resolvers SHOULD be stateless and re-entrant; the registry
|
|
37
35
|
# builds the chain once per `Analysis::Runner.run` and may
|
|
@@ -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
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "mutator"
|
|
6
|
+
require_relative "diagnostic_oracle"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Protection
|
|
10
|
+
# ADR-63 Tier 2 — the mutation *effectiveness* tier (the truth tier behind
|
|
11
|
+
# Tier 1's static {Inference::ProtectionScanner} proxy). For one file it
|
|
12
|
+
# answers the question Tier 1 only bounds: when a type-visible bug is
|
|
13
|
+
# introduced at a dispatch site, does Rigor actually catch it?
|
|
14
|
+
#
|
|
15
|
+
# Mechanism (the ADR-62 warm loop, narrowed to per-file measurement):
|
|
16
|
+
# generate the type-visible mutations ({Mutator}), keep only those whose
|
|
17
|
+
# receiver Rigor holds a concrete type for (the type-aware filter — the
|
|
18
|
+
# FP-safe meaning-maker; an unresolved receiver is kept), then for each ask
|
|
19
|
+
# the **kill oracle** whether the mutant is caught. The oracle is the ADR-69
|
|
20
|
+
# seam: {#scan_file} uses the {DiagnosticOracle} (a *new Rigor diagnostic* =
|
|
21
|
+
# a kill); {#scan_file_fused} additionally consults a {TestSuiteOracle} on the
|
|
22
|
+
# type-survivors (ADR-70 — the dynamic protection axis).
|
|
23
|
+
#
|
|
24
|
+
# The expensive builds (RBS environment + the whole-project pre-pass scan)
|
|
25
|
+
# are paid ONCE by the caller and threaded into the {DiagnosticOracle}; each
|
|
26
|
+
# mutant reuses them through `Runner.new(prebuilt:)#run_source` (in-memory
|
|
27
|
+
# overlay, no disk write).
|
|
28
|
+
class MutationScanner
|
|
29
|
+
# A surviving mutation site — a breakage Rigor did not catch.
|
|
30
|
+
SurvivingSite = Data.define(:line, :receiver, :method_name, :operator)
|
|
31
|
+
|
|
32
|
+
FileResult = Data.define(:path, :killed, :survived, :sites) do
|
|
33
|
+
# Mutations actually analysed (parse-invalid mutants are not counted).
|
|
34
|
+
def total = killed + survived
|
|
35
|
+
|
|
36
|
+
# Effectiveness ratio; a file with no type-relevant mutation is
|
|
37
|
+
# vacuously fully effective (no breakage was available to miss).
|
|
38
|
+
def ratio = total.zero? ? 1.0 : killed.to_f / total
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# ADR-70 — one type-survivor classified by the dynamic (test) axis.
|
|
42
|
+
# `protection` is `:test` (a test caught it) or `:none` (unprotected — the
|
|
43
|
+
# "add a type OR a test here" sites).
|
|
44
|
+
FusedSite = Data.define(:line, :receiver, :method_name, :operator, :protection)
|
|
45
|
+
|
|
46
|
+
# ADR-70 — the per-file fused classification. The gradual short-circuit
|
|
47
|
+
# collapses the conceptual "doubly-protected" bucket into `type_killed`:
|
|
48
|
+
# a mutant the type checker already kills never reaches the suite, because
|
|
49
|
+
# the static net already suffices and re-running the suite to learn a test
|
|
50
|
+
# *would also* catch it is wasted work. So the observed buckets are three.
|
|
51
|
+
FusedFileResult = Data.define(:path, :type_killed, :test_killed, :sites) do
|
|
52
|
+
# The unprotected sites (neither a type nor a test caught the breakage).
|
|
53
|
+
def unprotected = sites.size
|
|
54
|
+
def total = type_killed + test_killed + unprotected
|
|
55
|
+
|
|
56
|
+
# Fused protected ratio — caught by *either* axis.
|
|
57
|
+
def ratio = total.zero? ? 1.0 : (type_killed + test_killed).to_f / total
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param configuration [Rigor::Configuration]
|
|
61
|
+
# @param environment [Rigor::Environment] pre-built once by the caller
|
|
62
|
+
# @param project_scan [Rigor::Analysis::ProjectScan] pre-built once
|
|
63
|
+
# @param limit [Integer, nil] optional per-file mutation cap (sampled with
|
|
64
|
+
# `seed`); nil analyses every type-relevant mutation (deterministic).
|
|
65
|
+
# @param seed [Integer] RNG seed for the optional sample.
|
|
66
|
+
# @param oracle [#baseline, #killed?, nil] the kill oracle (ADR-69 Seam 1);
|
|
67
|
+
# defaults to the {DiagnosticOracle} (the ADR-62/63 behaviour).
|
|
68
|
+
# @param site_selector [:biteable, :all] which sites to mutate (ADR-69
|
|
69
|
+
# Seam 2). `:biteable` (default) keeps only concrete-type sites Rigor can
|
|
70
|
+
# bite; `:all` also mutates Dynamic-receiver dispatch sites — use only
|
|
71
|
+
# with a {TestSuiteOracle} (the fused overlay), never the diagnostic path.
|
|
72
|
+
def initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1, oracle: nil,
|
|
73
|
+
site_selector: :biteable)
|
|
74
|
+
@environment = environment
|
|
75
|
+
@limit = limit
|
|
76
|
+
@seed = seed
|
|
77
|
+
@site_selector = site_selector
|
|
78
|
+
@oracle = oracle || DiagnosticOracle.new(
|
|
79
|
+
configuration: configuration, environment: environment, project_scan: project_scan
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param path [String] the file to measure (used as the in-memory bind path)
|
|
84
|
+
# @param source [String, nil] the file's source; read from disk when nil
|
|
85
|
+
# @return [FileResult]
|
|
86
|
+
def scan_file(path, source: nil)
|
|
87
|
+
source ||= File.read(path, encoding: Encoding::UTF_8)
|
|
88
|
+
kept = kept_mutations(source, path)
|
|
89
|
+
return FileResult.new(path: path, killed: 0, survived: 0, sites: []) if kept.empty?
|
|
90
|
+
|
|
91
|
+
baseline = @oracle.baseline(source: source, path: path)
|
|
92
|
+
killed = 0
|
|
93
|
+
sites = []
|
|
94
|
+
kept.each do |mut|
|
|
95
|
+
case classify(source, path, mut, baseline)
|
|
96
|
+
when :killed then killed += 1
|
|
97
|
+
when :survived then sites << surviving_site(mut)
|
|
98
|
+
# :invalid — a parse-broken mutant; not a measurement, skip it.
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ADR-70 — the fused static∪dynamic measurement. Runs the type pass
|
|
105
|
+
# (the {DiagnosticOracle}); for every mutant the type checker did **not**
|
|
106
|
+
# kill, asks `test_oracle` whether the project's test suite catches it.
|
|
107
|
+
# The expensive suite run is paid only for type-survivors (the gradual
|
|
108
|
+
# short-circuit), so the cost is proportional to the protection hole.
|
|
109
|
+
# @param test_oracle [TestSuiteOracle]
|
|
110
|
+
# @return [FusedFileResult]
|
|
111
|
+
def scan_file_fused(path, test_oracle:, source: nil)
|
|
112
|
+
source ||= File.read(path, encoding: Encoding::UTF_8)
|
|
113
|
+
kept = kept_mutations(source, path)
|
|
114
|
+
return FusedFileResult.new(path: path, type_killed: 0, test_killed: 0, sites: []) if kept.empty?
|
|
115
|
+
|
|
116
|
+
baseline = @oracle.baseline(source: source, path: path)
|
|
117
|
+
type_killed = 0
|
|
118
|
+
test_killed = 0
|
|
119
|
+
sites = []
|
|
120
|
+
kept.each do |mut|
|
|
121
|
+
case classify(source, path, mut, baseline)
|
|
122
|
+
when :killed then type_killed += 1
|
|
123
|
+
when :survived
|
|
124
|
+
if test_oracle.killed?(path: path, original: source, mutant_source: mut.apply(source))
|
|
125
|
+
test_killed += 1
|
|
126
|
+
else
|
|
127
|
+
sites << fused_site(mut, :none)
|
|
128
|
+
end
|
|
129
|
+
# :invalid — a parse-broken mutant; not a measurement, skip it.
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
FusedFileResult.new(path: path, type_killed: type_killed, test_killed: test_killed, sites: sites)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# The mutations to measure: the biteable filter (concrete-type sites only;
|
|
138
|
+
# the FP-safe default — an unresolved receiver is kept) or, under the
|
|
139
|
+
# `:all` selector (ADR-69 Seam 2), every dispatch site including Dynamic
|
|
140
|
+
# receivers. Optionally sampled.
|
|
141
|
+
def kept_mutations(source, path)
|
|
142
|
+
mutator = Mutator.new(source)
|
|
143
|
+
muts = mutator.mutations
|
|
144
|
+
kept =
|
|
145
|
+
if @site_selector == :all
|
|
146
|
+
mutator.dispatch_site_mutations(muts, environment: @environment, path: path)
|
|
147
|
+
else
|
|
148
|
+
mutator.filter_by_type(muts, environment: @environment, path: path).first
|
|
149
|
+
end
|
|
150
|
+
sample(kept)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def classify(source, path, mut, baseline)
|
|
154
|
+
mutant_source = mut.apply(source)
|
|
155
|
+
return :invalid unless Prism.parse(mutant_source).success?
|
|
156
|
+
|
|
157
|
+
@oracle.killed?(mutant_source: mutant_source, path: path, baseline: baseline) ? :killed : :survived
|
|
158
|
+
rescue StandardError
|
|
159
|
+
# A harness-level failure on one mutant must not abort the file.
|
|
160
|
+
:invalid
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def sample(mutations)
|
|
164
|
+
return mutations unless @limit
|
|
165
|
+
|
|
166
|
+
mutations.sample(@limit, random: Random.new(@seed))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def surviving_site(mut)
|
|
170
|
+
SurvivingSite.new(line: mut.line, receiver: mut.anchor_type,
|
|
171
|
+
method_name: mut.method_name, operator: mut.operator.to_s)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def fused_site(mut, protection)
|
|
175
|
+
FusedSite.new(line: mut.line, receiver: mut.anchor_type, method_name: mut.method_name,
|
|
176
|
+
operator: mut.operator.to_s, protection: protection)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|