rigortype 0.1.18 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- 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/diagnostic.rb +8 -0
- 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 +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -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 +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -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 +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -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 +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- 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 +74 -5
- data/lib/rigor/environment.rb +17 -7
- 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/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- 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 +1072 -71
- 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/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- 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/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- 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 +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- 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 +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -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 +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- 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 +18 -49
- 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 +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- 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 +22 -35
- 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 +16 -23
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- 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 +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- 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 +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
data/lib/rigor/plugin/macro.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "macro/block_as_method"
|
|
4
|
-
require_relative "macro/external_file"
|
|
5
4
|
require_relative "macro/heredoc_template"
|
|
6
5
|
require_relative "macro/nested_class_template"
|
|
7
6
|
require_relative "macro/trait_registry"
|
|
@@ -11,15 +10,14 @@ module Rigor
|
|
|
11
10
|
# Substrate declarations for the macro / DSL expansion tiers
|
|
12
11
|
# introduced by ADR-16. Plugin authors declare entries under
|
|
13
12
|
# `Plugin::Manifest` slots (`block_as_methods:`,
|
|
14
|
-
# `trait_registries:`, `
|
|
15
|
-
# `
|
|
13
|
+
# `trait_registries:`, `heredoc_templates:`,
|
|
14
|
+
# `nested_class_templates:`) and the substrate consumes them
|
|
16
15
|
# to recognise the call shapes a library exposes to its users.
|
|
17
16
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# add files alongside `block_as_method.rb` without churn.
|
|
17
|
+
# Tier A (`BlockAsMethod`), Tier B (`TraitRegistry`), Tier C
|
|
18
|
+
# (`HeredocTemplate`), and ADR-36 nested-class emission
|
|
19
|
+
# (`NestedClassTemplate`) value classes are all shipped here.
|
|
20
|
+
# Engine wiring lives in `lib/rigor/inference/`.
|
|
23
21
|
#
|
|
24
22
|
# Per ADR-16 § WD13, substrate-produced output ships at a
|
|
25
23
|
# **floor** in v0.1.x ("substrate-affected code parses cleanly
|
|
@@ -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)
|
|
@@ -43,7 +43,7 @@ module Rigor
|
|
|
43
43
|
|
|
44
44
|
attr_reader :id, :version, :description, :config_schema, :config_defaults, :produces, :consumes,
|
|
45
45
|
:owns_receivers, :open_receivers, :type_node_resolvers, :block_as_methods,
|
|
46
|
-
:heredoc_templates, :nested_class_templates, :trait_registries,
|
|
46
|
+
:heredoc_templates, :nested_class_templates, :trait_registries,
|
|
47
47
|
:hkt_registrations, :hkt_definitions, :signature_paths, :protocol_contracts,
|
|
48
48
|
:source_rbs_synthesizer, :additional_initializers
|
|
49
49
|
|
|
@@ -52,7 +52,7 @@ module Rigor
|
|
|
52
52
|
description: nil, config_schema: {},
|
|
53
53
|
produces: [], consumes: [], owns_receivers: [], open_receivers: [], type_node_resolvers: [],
|
|
54
54
|
block_as_methods: [], heredoc_templates: [], nested_class_templates: [],
|
|
55
|
-
trait_registries: [],
|
|
55
|
+
trait_registries: [],
|
|
56
56
|
hkt_registrations: [], hkt_definitions: [], signature_paths: [], protocol_contracts: [],
|
|
57
57
|
source_rbs_synthesizer: nil, additional_initializers: []
|
|
58
58
|
)
|
|
@@ -67,7 +67,6 @@ module Rigor
|
|
|
67
67
|
validate_heredoc_templates!(heredoc_templates)
|
|
68
68
|
validate_nested_class_templates!(nested_class_templates)
|
|
69
69
|
validate_trait_registries!(trait_registries)
|
|
70
|
-
validate_external_files!(external_files)
|
|
71
70
|
validate_hkt_registrations!(hkt_registrations)
|
|
72
71
|
validate_hkt_definitions!(hkt_definitions)
|
|
73
72
|
validate_signature_paths!(signature_paths)
|
|
@@ -77,7 +76,7 @@ module Rigor
|
|
|
77
76
|
|
|
78
77
|
assign_fields(id, version, description, config_schema, produces, consumes, owns_receivers,
|
|
79
78
|
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
80
|
-
|
|
79
|
+
hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
|
|
81
80
|
source_rbs_synthesizer)
|
|
82
81
|
assign_nested_class_templates(nested_class_templates)
|
|
83
82
|
assign_additional_initializers(additional_initializers)
|
|
@@ -89,7 +88,7 @@ module Rigor
|
|
|
89
88
|
# rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
|
|
90
89
|
def assign_fields(id, version, description, config_schema, produces, consumes, owns_receivers,
|
|
91
90
|
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
92
|
-
|
|
91
|
+
hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
|
|
93
92
|
source_rbs_synthesizer)
|
|
94
93
|
@id = id.dup.freeze
|
|
95
94
|
@version = version.dup.freeze
|
|
@@ -104,7 +103,6 @@ module Rigor
|
|
|
104
103
|
@block_as_methods = block_as_methods.dup.freeze
|
|
105
104
|
@heredoc_templates = heredoc_templates.dup.freeze
|
|
106
105
|
@trait_registries = trait_registries.dup.freeze
|
|
107
|
-
@external_files = external_files.dup.freeze
|
|
108
106
|
@hkt_registrations = hkt_registrations.dup.freeze
|
|
109
107
|
@hkt_definitions = hkt_definitions.dup.freeze
|
|
110
108
|
@signature_paths = signature_paths.map { |p| p.to_s.dup.freeze }.freeze
|
|
@@ -170,7 +168,6 @@ module Rigor
|
|
|
170
168
|
"heredoc_templates" => heredoc_templates.map(&:to_h),
|
|
171
169
|
"nested_class_templates" => nested_class_templates.map(&:to_h),
|
|
172
170
|
"trait_registries" => trait_registries.map(&:to_h),
|
|
173
|
-
"external_files" => external_files.map(&:to_h),
|
|
174
171
|
"hkt_registrations" => hkt_registrations.map(&:to_h),
|
|
175
172
|
"hkt_definitions" => hkt_definitions.map { |d| { "uri" => d.uri, "params" => d.params } },
|
|
176
173
|
"signature_paths" => signature_paths,
|
|
@@ -280,10 +277,18 @@ module Rigor
|
|
|
280
277
|
end
|
|
281
278
|
end
|
|
282
279
|
|
|
283
|
-
|
|
284
|
-
|
|
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?(&)
|
|
285
286
|
|
|
286
|
-
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) }
|
|
287
292
|
end
|
|
288
293
|
|
|
289
294
|
# ADR-10 5a — `owns_receivers:` declares the class names
|
|
@@ -294,11 +299,7 @@ module Rigor
|
|
|
294
299
|
# so plugin contributions stay authoritative for those
|
|
295
300
|
# types.
|
|
296
301
|
def validate_owns_receivers!(owns_receivers)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
raise ArgumentError,
|
|
300
|
-
"plugin manifest owns_receivers must be an Array of non-empty String, " \
|
|
301
|
-
"got #{owns_receivers.inspect}"
|
|
302
|
+
validate_array_of!("owns_receivers", owns_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
|
|
302
303
|
end
|
|
303
304
|
|
|
304
305
|
# ADR-26 — `open_receivers:` declares the class names this
|
|
@@ -312,11 +313,7 @@ module Rigor
|
|
|
312
313
|
# `owns_receivers:` (which routes dispatch); this one only
|
|
313
314
|
# suppresses the diagnostic.
|
|
314
315
|
def validate_open_receivers!(open_receivers)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
raise ArgumentError,
|
|
318
|
-
"plugin manifest open_receivers must be an Array of non-empty String, " \
|
|
319
|
-
"got #{open_receivers.inspect}"
|
|
316
|
+
validate_array_of!("open_receivers", open_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
|
|
320
317
|
end
|
|
321
318
|
|
|
322
319
|
# ADR-13 slice 2 — `type_node_resolvers:` declares the
|
|
@@ -328,11 +325,9 @@ module Rigor
|
|
|
328
325
|
# integration that actually drives the chain lands in
|
|
329
326
|
# slice 3.
|
|
330
327
|
def validate_type_node_resolvers!(resolvers)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
"plugin manifest type_node_resolvers must be an Array of " \
|
|
335
|
-
"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
|
|
336
331
|
end
|
|
337
332
|
|
|
338
333
|
# ADR-16 slice 1a — `block_as_methods:` declares the Tier A
|
|
@@ -341,11 +336,9 @@ module Rigor
|
|
|
341
336
|
# actually narrows `Scope#self_type` for matching blocks
|
|
342
337
|
# arrives in a subsequent slice.
|
|
343
338
|
def validate_block_as_methods!(entries)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"plugin manifest block_as_methods must be an Array of " \
|
|
348
|
-
"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
|
|
349
342
|
end
|
|
350
343
|
|
|
351
344
|
# ADR-16 slice 2a — `heredoc_templates:` declares the Tier C
|
|
@@ -354,11 +347,9 @@ module Rigor
|
|
|
354
347
|
# manifest; the pre-pass + `SyntheticMethodIndex` that actually
|
|
355
348
|
# emit synthetic methods arrive in slice 2b.
|
|
356
349
|
def validate_heredoc_templates!(entries)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
"plugin manifest heredoc_templates must be an Array of " \
|
|
361
|
-
"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
|
|
362
353
|
end
|
|
363
354
|
|
|
364
355
|
# ADR-36 — `nested_class_templates:` declares the
|
|
@@ -368,11 +359,10 @@ module Rigor
|
|
|
368
359
|
# subclasses + their `#inner` reader through the existing
|
|
369
360
|
# `SyntheticMethodIndex` primitive.
|
|
370
361
|
def validate_nested_class_templates!(entries)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
"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
|
|
376
366
|
end
|
|
377
367
|
|
|
378
368
|
# ADR-16 slice 3a — `trait_registries:` declares the Tier B
|
|
@@ -382,28 +372,9 @@ module Rigor
|
|
|
382
372
|
# `SyntheticMethodIndex` (slice 2b primitive) arrives in
|
|
383
373
|
# slice 3b.
|
|
384
374
|
def validate_trait_registries!(entries)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"plugin manifest trait_registries must be an Array of " \
|
|
389
|
-
"Rigor::Plugin::Macro::TraitRegistry instances, got #{entries.inspect}"
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
# ADR-16 slice 5a — `external_files:` declares the Tier D
|
|
393
|
-
# substrate entries (external-Ruby-file inclusion under a
|
|
394
|
-
# declared `self`). Slice 5a carries the declarations on
|
|
395
|
-
# the manifest; the engine integration that walks the
|
|
396
|
-
# matched files + narrows their entry scope is **queued for
|
|
397
|
-
# slice 5b**, gated on demonstrated demand from concrete
|
|
398
|
-
# plugin targets (Redmine webhook payloads, tDiary plugin
|
|
399
|
-
# loader, etc.). Plugin authors MAY declare entries today;
|
|
400
|
-
# the substrate does not yet act on them.
|
|
401
|
-
def validate_external_files!(entries)
|
|
402
|
-
return if entries.is_a?(Array) && entries.all?(Macro::ExternalFile)
|
|
403
|
-
|
|
404
|
-
raise ArgumentError,
|
|
405
|
-
"plugin manifest external_files must be an Array of " \
|
|
406
|
-
"Rigor::Plugin::Macro::ExternalFile 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
|
|
407
378
|
end
|
|
408
379
|
|
|
409
380
|
# ADR-20 slice 6 — `hkt_registrations:` declares the
|
|
@@ -418,11 +389,9 @@ module Rigor
|
|
|
418
389
|
# user `.rbs` overlays merge on top of plugin entries
|
|
419
390
|
# last-write-wins.
|
|
420
391
|
def validate_hkt_registrations!(entries)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
"plugin manifest hkt_registrations must be an Array of " \
|
|
425
|
-
"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
|
|
426
395
|
end
|
|
427
396
|
|
|
428
397
|
# ADR-20 slice 6 — `hkt_definitions:` declares the
|
|
@@ -435,11 +404,9 @@ module Rigor
|
|
|
435
404
|
# via {Rigor::Inference::HktBody}'s node-constructor API
|
|
436
405
|
# without parsing a string.
|
|
437
406
|
def validate_hkt_definitions!(entries)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
"plugin manifest hkt_definitions must be an Array of " \
|
|
442
|
-
"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
|
|
443
410
|
end
|
|
444
411
|
|
|
445
412
|
# ADR-25 — `signature_paths:` declares the RBS signature
|
|
@@ -449,11 +416,7 @@ module Rigor
|
|
|
449
416
|
# loader validates each exists and `Environment.for_project`
|
|
450
417
|
# merges the resolved set into the RBS environment.
|
|
451
418
|
def validate_signature_paths!(paths)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
raise ArgumentError,
|
|
455
|
-
"plugin manifest signature_paths must be an Array of non-empty String, " \
|
|
456
|
-
"got #{paths.inspect}"
|
|
419
|
+
validate_array_of!("signature_paths", paths, "non-empty String") { |p| p.is_a?(String) && !p.empty? }
|
|
457
420
|
end
|
|
458
421
|
|
|
459
422
|
# ADR-28 — `protocol_contracts:` declares the path-scoped
|
|
@@ -469,11 +432,9 @@ module Rigor
|
|
|
469
432
|
# MAY override `Plugin::Base#protocol_contracts` to fold in
|
|
470
433
|
# per-project config (e.g. a custom convention path).
|
|
471
434
|
def validate_protocol_contracts!(entries)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
"plugin manifest protocol_contracts must be an Array of " \
|
|
476
|
-
"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
|
|
477
438
|
end
|
|
478
439
|
|
|
479
440
|
# ADR-38 — `additional_initializers:` declares the
|
|
@@ -485,11 +446,9 @@ module Rigor
|
|
|
485
446
|
# loaded plugins; `Inference::ScopeIndexer` consults the set at
|
|
486
447
|
# its single gate.
|
|
487
448
|
def validate_additional_initializers!(entries)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"plugin manifest additional_initializers must be an Array of " \
|
|
492
|
-
"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
|
|
493
452
|
end
|
|
494
453
|
|
|
495
454
|
# ADR-32 WD4 — `source_rbs_synthesizer:` declares a callable
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "node_context"
|
|
4
4
|
require_relative "../source/node_walker"
|
|
5
|
+
require_relative "../analysis/check_rules/rule_walk"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Plugin
|
|
@@ -64,30 +65,74 @@ module Rigor
|
|
|
64
65
|
# Walk `root` once, dispatching every node to each matching
|
|
65
66
|
# `(plugin, rule)`. Returns an Array of {Result} in plugin
|
|
66
67
|
# (registry) order. `root` nil yields one empty Result per plugin.
|
|
67
|
-
|
|
68
|
+
#
|
|
69
|
+
# ADR-53 B4 — when `collector_driver` is given (an
|
|
70
|
+
# {Analysis::CheckRules::RuleWalk::CollectorDriver}), the SAME
|
|
71
|
+
# single traversal also drives the built-in {CheckRules} node
|
|
72
|
+
# collectors: each visited node is dispatched both to the plugin
|
|
73
|
+
# rules (this walk's original job) and to the built-in collectors
|
|
74
|
+
# (the `CollectorDriver`), so a file is walked once for both
|
|
75
|
+
# instead of once each. The two dispatch models coexist: plugin
|
|
76
|
+
# rules keep `is_a?` matching via the per-class memo and receive a
|
|
77
|
+
# lazily-built {NodeContext} (ancestors); built-in collectors keep
|
|
78
|
+
# exact-node-class dispatch and receive the immutable
|
|
79
|
+
# {RuleWalk::Context} threaded through the descent. Order is
|
|
80
|
+
# preserved because each side accumulates into its own bucket
|
|
81
|
+
# (per-plugin {Result}s / per-collector `results`) and the two are
|
|
82
|
+
# assembled separately by their respective diagnostic builders.
|
|
83
|
+
# A raising plugin rule isolates only that plugin (per-{State}
|
|
84
|
+
# rescue) and never aborts built-in collection, nor vice versa
|
|
85
|
+
# (the collectors' `visit` is the verbatim legacy gather logic,
|
|
86
|
+
# which does not raise on the corpora).
|
|
87
|
+
def diagnostics_for_file(path:, scope:, root:, collector_driver: nil)
|
|
68
88
|
return @entries.map { |plugin, _| Result.new(plugin, [], nil) } if root.nil?
|
|
69
89
|
|
|
70
90
|
states = @entries.map { |plugin, rules| State.new(plugin, rules, scope, root) }
|
|
71
|
-
walk(path, scope, root, states)
|
|
91
|
+
walk(path, scope, root, states, collector_driver)
|
|
72
92
|
states.map(&:result)
|
|
73
93
|
end
|
|
74
94
|
|
|
75
95
|
private
|
|
76
96
|
|
|
77
|
-
def walk(path, scope, root, states)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
def walk(path, scope, root, states, collector_driver)
|
|
98
|
+
context = collector_driver ? Analysis::CheckRules::RuleWalk::Context.root : nil
|
|
99
|
+
walk_node(root, [], context, path, scope, states, collector_driver)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The single converged DFS pre-order traversal. Threads both the
|
|
103
|
+
# live `ancestors` stack (for plugin {NodeContext}) and the
|
|
104
|
+
# immutable built-in {RuleWalk::Context} (for the collectors),
|
|
105
|
+
# derived together as the walk descends — the cheap-ancestors
|
|
106
|
+
# option from the ADR-53 B4 design note. Identical pre-order over
|
|
107
|
+
# `compact_child_nodes` to both the legacy
|
|
108
|
+
# `Source::NodeWalker.each_with_ancestors` and `RuleWalk.walk`, so
|
|
109
|
+
# every node is visited in the same order each side saw before.
|
|
110
|
+
def walk_node(node, ancestors, context, path, scope, states, collector_driver)
|
|
111
|
+
return unless node.is_a?(Prism::Node)
|
|
112
|
+
|
|
113
|
+
dispatch_plugins(node, ancestors, path, scope, states)
|
|
114
|
+
collector_driver&.visit(node, context)
|
|
115
|
+
|
|
116
|
+
child_context = collector_driver&.descend(node, context)
|
|
117
|
+
ancestors.push(node)
|
|
118
|
+
node.compact_child_nodes.each do |child|
|
|
119
|
+
walk_node(child, ancestors, child_context, path, scope, states, collector_driver)
|
|
120
|
+
end
|
|
121
|
+
ancestors.pop
|
|
122
|
+
end
|
|
82
123
|
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
def dispatch_plugins(node, ancestors, path, scope, states)
|
|
125
|
+
node_context = nil
|
|
126
|
+
states.each do |state|
|
|
127
|
+
next if state.failed?
|
|
85
128
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
matched = state.rules_for(node)
|
|
130
|
+
next if matched.empty?
|
|
131
|
+
|
|
132
|
+
# One frozen NodeContext per node, built lazily and shared
|
|
133
|
+
# across every plugin that matches this node.
|
|
134
|
+
node_context ||= NodeContext.new(ancestors)
|
|
135
|
+
state.run_rules(matched, node, scope, path, node_context)
|
|
91
136
|
end
|
|
92
137
|
end
|
|
93
138
|
|
|
@@ -7,7 +7,7 @@ module Rigor
|
|
|
7
7
|
# ADR-52 WD1 — the compiled contribution table. Categorises a loaded
|
|
8
8
|
# plugin set by which per-call contribution paths each plugin
|
|
9
9
|
# actually implements, AND compiles the declarative gates (method
|
|
10
|
-
# names, `block_as_methods`
|
|
10
|
+
# names, `block_as_methods` method names, `owns_receivers`) into frozen
|
|
11
11
|
# lookup structures, so the engine's hot sites discover "no plugin
|
|
12
12
|
# cares about this call" in O(1) instead of O(plugins × rules) — a
|
|
13
13
|
# top hotspot on plugin-heavy projects (GitLab's 11 plugins, of
|
|
@@ -16,7 +16,7 @@ module Rigor
|
|
|
16
16
|
#
|
|
17
17
|
# Ordering contract: the gates only PRUNE consultations that could
|
|
18
18
|
# not fire (every pruned rule would have failed its own `methods:` /
|
|
19
|
-
# `
|
|
19
|
+
# `method_names:` check); the engine still iterates the plugin subsets in
|
|
20
20
|
# registry order and each plugin's rules in declaration order, so
|
|
21
21
|
# the surviving contributions arrive in exactly the order the
|
|
22
22
|
# ungated walk produced — diagnostics stay byte-identical. The
|
|
@@ -36,7 +36,7 @@ module Rigor
|
|
|
36
36
|
def initialize(plugins)
|
|
37
37
|
compile_memberships(plugins)
|
|
38
38
|
compile_gates
|
|
39
|
-
@
|
|
39
|
+
@block_entries_by_method_name = build_block_entries(plugins)
|
|
40
40
|
@owns_receivers = plugins.flat_map { |p| manifest_for(p)&.owns_receivers || [] }.uniq.freeze
|
|
41
41
|
# Per-run ancestry verdict memo, keyed by environment identity
|
|
42
42
|
# then class name. Mutable inside the frozen index — sound
|
|
@@ -88,11 +88,12 @@ module Rigor
|
|
|
88
88
|
gate.nil? || gate.include?(method_name)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
# The `Macro::BlockAsMethod` entries whose `
|
|
92
|
-
# in (plugin registration, manifest declaration)
|
|
93
|
-
# first-match order the previous plugins ×
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
# The `Macro::BlockAsMethod` entries whose `method_names` include
|
|
92
|
+
# `method_name`, in (plugin registration, manifest declaration)
|
|
93
|
+
# order — the same first-match order the previous plugins ×
|
|
94
|
+
# entries walk visited.
|
|
95
|
+
def block_entries_for(method_name)
|
|
96
|
+
@block_entries_by_method_name.fetch(method_name, EMPTY_BLOCK_ENTRIES)
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
# True when `class_name` equals or inherits from any plugin's
|
|
@@ -188,15 +189,15 @@ module Rigor
|
|
|
188
189
|
gates.values.reduce(Set.new) { |acc, names| acc.merge(names) }.freeze
|
|
189
190
|
end
|
|
190
191
|
|
|
191
|
-
# `
|
|
192
|
-
# (plugin, declaration).
|
|
192
|
+
# `method-name Symbol → [BlockAsMethod entries]`, insertion-ordered
|
|
193
|
+
# by (plugin, declaration). Method names are Symbol-normalised by
|
|
193
194
|
# `Macro::BlockAsMethod#initialize`.
|
|
194
195
|
def build_block_entries(plugins)
|
|
195
196
|
table = {}
|
|
196
197
|
plugins.each do |plugin|
|
|
197
198
|
entries = manifest_for(plugin)&.block_as_methods || []
|
|
198
199
|
entries.each do |entry|
|
|
199
|
-
entry.
|
|
200
|
+
entry.method_names.each { |name| (table[name] ||= []) << entry }
|
|
200
201
|
end
|
|
201
202
|
end
|
|
202
203
|
table.each_value(&:freeze)
|
|
@@ -316,13 +317,12 @@ module Rigor
|
|
|
316
317
|
!load_errors.empty?
|
|
317
318
|
end
|
|
318
319
|
|
|
319
|
-
# ADR-13
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
# — 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.
|
|
326
326
|
def type_node_resolvers
|
|
327
327
|
plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
|
|
328
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,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../analysis/runner"
|
|
6
|
+
require_relative "mutator"
|
|
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:
|
|
19
|
+
# re-analyse the mutated SOURCE against a clean baseline and read whether a
|
|
20
|
+
# NEW diagnostic appears. A *killed* mutation is a caught breakage; a
|
|
21
|
+
# *survived* one is a breakage Rigor missed — an "add a type here" site.
|
|
22
|
+
#
|
|
23
|
+
# The expensive builds (RBS environment + the whole-project pre-pass scan)
|
|
24
|
+
# are paid ONCE by the caller and threaded in via `environment:` /
|
|
25
|
+
# `project_scan:`; each mutant reuses them through
|
|
26
|
+
# `Runner.new(prebuilt:)#run_source` (in-memory overlay, no disk write).
|
|
27
|
+
# Passing `prebuilt:` disables the run-result cache (whose key digests the
|
|
28
|
+
# *disk* file), so a mutant is never served a stale clean hit.
|
|
29
|
+
class MutationScanner
|
|
30
|
+
# A surviving mutation site — a breakage Rigor did not catch.
|
|
31
|
+
SurvivingSite = Data.define(:line, :receiver, :method_name, :operator)
|
|
32
|
+
|
|
33
|
+
FileResult = Data.define(:path, :killed, :survived, :sites) do
|
|
34
|
+
# Mutations actually analysed (parse-invalid mutants are not counted).
|
|
35
|
+
def total = killed + survived
|
|
36
|
+
|
|
37
|
+
# Effectiveness ratio; a file with no type-relevant mutation is
|
|
38
|
+
# vacuously fully effective (no breakage was available to miss).
|
|
39
|
+
def ratio = total.zero? ? 1.0 : killed.to_f / total
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param configuration [Rigor::Configuration]
|
|
43
|
+
# @param environment [Rigor::Environment] pre-built once by the caller
|
|
44
|
+
# @param project_scan [Rigor::Analysis::ProjectScan] pre-built once
|
|
45
|
+
# @param limit [Integer, nil] optional per-file mutation cap (sampled with
|
|
46
|
+
# `seed`); nil analyses every type-relevant mutation (deterministic).
|
|
47
|
+
# @param seed [Integer] RNG seed for the optional sample.
|
|
48
|
+
def initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1)
|
|
49
|
+
@configuration = configuration
|
|
50
|
+
@environment = environment
|
|
51
|
+
@project_scan = project_scan
|
|
52
|
+
@limit = limit
|
|
53
|
+
@seed = seed
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param path [String] the file to measure (used as the in-memory bind path)
|
|
57
|
+
# @param source [String, nil] the file's source; read from disk when nil
|
|
58
|
+
# @return [FileResult]
|
|
59
|
+
def scan_file(path, source: nil)
|
|
60
|
+
source ||= File.read(path, encoding: Encoding::UTF_8)
|
|
61
|
+
mutator = Mutator.new(source)
|
|
62
|
+
kept, = mutator.filter_by_type(mutator.mutations, environment: @environment, path: path)
|
|
63
|
+
kept = sample(kept)
|
|
64
|
+
return FileResult.new(path: path, killed: 0, survived: 0, sites: []) if kept.empty?
|
|
65
|
+
|
|
66
|
+
baseline = signatures(analyse(source, path))
|
|
67
|
+
measure(source, path, kept, baseline)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def measure(source, path, mutations, baseline)
|
|
73
|
+
killed = 0
|
|
74
|
+
sites = []
|
|
75
|
+
mutations.each do |mut|
|
|
76
|
+
case classify(source, path, mut, baseline)
|
|
77
|
+
when :killed then killed += 1
|
|
78
|
+
when :survived then sites << surviving_site(mut)
|
|
79
|
+
# :invalid — a parse-broken mutant; not a measurement, skip it.
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def classify(source, path, mut, baseline)
|
|
86
|
+
mutant_source = mut.apply(source)
|
|
87
|
+
return :invalid unless Prism.parse(mutant_source).success?
|
|
88
|
+
|
|
89
|
+
new_diagnostics = analyse(mutant_source, path).reject { |d| baseline.include?(sig(d)) }
|
|
90
|
+
new_diagnostics.empty? ? :survived : :killed
|
|
91
|
+
rescue StandardError
|
|
92
|
+
# A harness-level failure on one mutant must not abort the file.
|
|
93
|
+
:invalid
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# cache_store: nil + prebuilt: scan ⇒ the run cache is bypassed and the
|
|
97
|
+
# mutant is always re-analysed against the in-memory bytes.
|
|
98
|
+
def analyse(source, path)
|
|
99
|
+
Rigor::Analysis::Runner.new(
|
|
100
|
+
configuration: @configuration, environment: @environment, prebuilt: @project_scan,
|
|
101
|
+
cache_store: nil, collect_stats: false
|
|
102
|
+
).run_source(source: source, path: path).diagnostics
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def sample(mutations)
|
|
106
|
+
return mutations unless @limit
|
|
107
|
+
|
|
108
|
+
mutations.sample(@limit, random: Random.new(@seed))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def signatures(diagnostics) = diagnostics.to_set { |d| sig(d) }
|
|
112
|
+
def sig(diagnostic) = [diagnostic.rule, diagnostic.path, diagnostic.line, diagnostic.column, diagnostic.message]
|
|
113
|
+
|
|
114
|
+
def surviving_site(mut)
|
|
115
|
+
SurvivingSite.new(line: mut.line, receiver: mut.anchor_type,
|
|
116
|
+
method_name: mut.method_name, operator: mut.operator.to_s)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|