rigortype 0.3.5 → 0.3.7
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 +5 -5
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +298 -38
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -19,7 +19,14 @@ module Rigor
|
|
|
19
19
|
# Slice 2 ships a minimal surface:
|
|
20
20
|
#
|
|
21
21
|
# - `#read_file(path)` — validates against the policy, returns the file's contents, and adds a
|
|
22
|
-
# digest-keyed {Cache::Descriptor::FileEntry} to the boundary's accumulated descriptor.
|
|
22
|
+
# digest-keyed {Cache::Descriptor::FileEntry} to the boundary's accumulated descriptor. A read that
|
|
23
|
+
# fails because the path does not exist adds an ABSENCE row instead (ADR-45 WD1, #577), so a result
|
|
24
|
+
# computed on the file being missing invalidates once it appears.
|
|
25
|
+
# - `#file?(path)` / `#directory?(path)` — the existence PROBE (ADR-45 WD1b, #613): the same answer
|
|
26
|
+
# `File.file?` / `File.directory?` gives, plus the existence row `#read_file` would have recorded.
|
|
27
|
+
# A plugin that gates its read or its glob on one of these shapes its result on the answer, so the
|
|
28
|
+
# answer is a dependency; probing through `File` directly leaves that dependency unrecorded and the
|
|
29
|
+
# warm run serves the pre-appearance result.
|
|
23
30
|
# - `#open_url(url)` — fetches the URL when the policy permits it (`network_policy: :allowlist` plus an
|
|
24
31
|
# `allowed_url_hosts` match) and raises {AccessDeniedError} otherwise. v0.1.2 ships the allowlist
|
|
25
32
|
# surface; the default project policy still has `network_policy: :disabled` so plugins that want network
|
|
@@ -46,8 +53,18 @@ module Rigor
|
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
# Reads the file at `path` after validating it against the policy. Raises {AccessDeniedError} when the
|
|
49
|
-
# path is outside every allowed read root. Records a `:
|
|
50
|
-
# invalidates on content change.
|
|
56
|
+
# path is outside every allowed read root. Records a `:stat` {Cache::Descriptor::FileEntry} so the
|
|
57
|
+
# resulting cache slice invalidates on content change.
|
|
58
|
+
#
|
|
59
|
+
# ADR-45 WD1 (#577) — a read that fails because the path does NOT exist (`Errno::ENOENT`, or
|
|
60
|
+
# `Errno::ENOTDIR` when a parent component is a regular file) is a deliberate existence probe: the
|
|
61
|
+
# plugin asked for the file and shaped its answer on the file being absent (rigor-activerecord's
|
|
62
|
+
# reduced mode on a missing `db/schema.rb`). That is a dependency too — "the analysis depended on X
|
|
63
|
+
# being absent" — so the boundary records an ABSENCE row ({Cache::Descriptor::FileEntry.absent})
|
|
64
|
+
# before re-raising, and every cache built on this boundary's descriptor (the run-result entry, the
|
|
65
|
+
# plugin-producer entries) goes stale the moment the path comes into existence. Only the not-there
|
|
66
|
+
# outcome is recorded: a path that exists but cannot be read (`EISDIR`, `EACCES`) is a failure the
|
|
67
|
+
# plugin reports, not a probe of absence, and records nothing — as before.
|
|
51
68
|
def read_file(path)
|
|
52
69
|
absolute = File.expand_path(path.to_s)
|
|
53
70
|
unless @policy.allow_read?(absolute)
|
|
@@ -59,11 +76,39 @@ module Rigor
|
|
|
59
76
|
)
|
|
60
77
|
end
|
|
61
78
|
|
|
62
|
-
contents =
|
|
79
|
+
contents = begin
|
|
80
|
+
File.binread(absolute)
|
|
81
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
82
|
+
record_absence_entry(absolute)
|
|
83
|
+
raise
|
|
84
|
+
end
|
|
63
85
|
record_file_entry(absolute, contents)
|
|
64
86
|
contents
|
|
65
87
|
end
|
|
66
88
|
|
|
89
|
+
# ADR-45 WD1b (#613) — the existence probe. Returns exactly what `File.file?(path)` returns, and
|
|
90
|
+
# records the answer as a {Cache::Descriptor::FileEntry} `:exists` row so a plugin that gates its read
|
|
91
|
+
# on the probe carries the same dependency a plugin that just called {#read_file} carries.
|
|
92
|
+
# `File.file?` in plugin code records nothing, which is how the #577 staleness class survived at every
|
|
93
|
+
# `return nil unless File.file?(config)` gate: the miss shaped the result and left no edge for the
|
|
94
|
+
# file's later appearance.
|
|
95
|
+
#
|
|
96
|
+
# @param path [String] project path; relative paths expand against the working directory
|
|
97
|
+
# @return [Boolean] `File.file?`'s answer, unchanged
|
|
98
|
+
def file?(path)
|
|
99
|
+
probe(path) { |absolute| File.file?(absolute) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ADR-45 WD1b (#613) — {#file?} for directories: the answer `File.directory?` gives, plus the
|
|
103
|
+
# existence row. The discovery shape (`next [] unless directory?(root)` before a `Dir.glob`) depends
|
|
104
|
+
# on the root existing exactly as a config read depends on the config file existing.
|
|
105
|
+
#
|
|
106
|
+
# @param path [String] project path; relative paths expand against the working directory
|
|
107
|
+
# @return [Boolean] `File.directory?`'s answer, unchanged
|
|
108
|
+
def directory?(path)
|
|
109
|
+
probe(path) { |absolute| File.directory?(absolute) }
|
|
110
|
+
end
|
|
111
|
+
|
|
67
112
|
# Fetches the URL when the policy permits it. Returns the response body. Raises {AccessDeniedError} when
|
|
68
113
|
# the policy is `:disabled`, the URL scheme is not `https`, the host is not on the allowlist, the
|
|
69
114
|
# response is non-2xx, the body exceeds {URL_MAX_BYTES}, or the request times out
|
|
@@ -96,6 +141,36 @@ module Rigor
|
|
|
96
141
|
|
|
97
142
|
private
|
|
98
143
|
|
|
144
|
+
# ADR-45 WD1b (#613) — the shared body of {#file?} / {#directory?}.
|
|
145
|
+
#
|
|
146
|
+
# The probe answers TRUTHFULLY for every path, in or out of the trusted-read scope, and never raises:
|
|
147
|
+
# it replaces a bare `File.file?` in plugin code, and a predicate that raised (or answered `false` for
|
|
148
|
+
# an out-of-scope path that exists) would change what the converted plugin does rather than only what
|
|
149
|
+
# it records. Existence is not content — ADR-2's boundary is not a sandbox, and the plugin could ask
|
|
150
|
+
# `File` directly — so the policy governs RECORDING here, not the answer: an out-of-scope path
|
|
151
|
+
# contributes no dependency row, exactly as an out-of-scope read contributes none.
|
|
152
|
+
#
|
|
153
|
+
# Three outcomes, mirroring {#read_file}'s bounds:
|
|
154
|
+
#
|
|
155
|
+
# - the probe found what it asked for → a PRESENCE row (`:exists` / `"true"`), stale once the path is
|
|
156
|
+
# gone;
|
|
157
|
+
# - the probe found nothing at all → an ABSENCE row, stale once anything appears there;
|
|
158
|
+
# - the path exists but is not what was asked for (a directory where a file was wanted, or the
|
|
159
|
+
# reverse) → NOTHING, the same bound `#read_file` pins for `EISDIR`: the probe observed neither a
|
|
160
|
+
# usable file nor an absence, and either row would misdescribe what the plugin saw.
|
|
161
|
+
def probe(path)
|
|
162
|
+
absolute = File.expand_path(path.to_s)
|
|
163
|
+
answer = yield(absolute)
|
|
164
|
+
return answer unless @policy.allow_read?(absolute)
|
|
165
|
+
|
|
166
|
+
if answer
|
|
167
|
+
record_presence_entry(absolute)
|
|
168
|
+
elsif !File.exist?(absolute)
|
|
169
|
+
record_absence_entry(absolute)
|
|
170
|
+
end
|
|
171
|
+
answer
|
|
172
|
+
end
|
|
173
|
+
|
|
99
174
|
def record_file_entry(path, contents)
|
|
100
175
|
# ADR-87 WD1 — the boundary descriptor is validation-only (it never keys a cache), so a plugin-read
|
|
101
176
|
# file rides the stat-then-digest `:stat` tier: a warm run stat-checks the (on a Rails monorepo,
|
|
@@ -103,9 +178,37 @@ module Rigor
|
|
|
103
178
|
# we just read; `FileEntry.stat` stats the same path to pack the tuple.
|
|
104
179
|
digest = Digest::SHA256.hexdigest(contents)
|
|
105
180
|
entry = Cache::Descriptor::FileEntry.stat(path: path, digest: digest)
|
|
181
|
+
# A content row replaces an earlier existence row for the same path outright: the file appeared and
|
|
182
|
+
# its bytes were consumed, and a content row validates existence as well as content.
|
|
106
183
|
@mutex.synchronize { @file_entries[path] = entry }
|
|
107
184
|
end
|
|
108
185
|
|
|
186
|
+
# ADR-45 WD1 (#577) — the absence row for a probed-but-missing path. A content row already recorded
|
|
187
|
+
# for the same path is KEPT in preference: two outcomes for one path within one run mean the file
|
|
188
|
+
# moved under the analysis, and of the two rows the content row is the one whose validation covers
|
|
189
|
+
# both the content that was read and the file's existence — so it is the one that reads stale while
|
|
190
|
+
# the file is gone. The reverse order (probed absent, then read once it appeared) is the content
|
|
191
|
+
# row's replacement above.
|
|
192
|
+
#
|
|
193
|
+
# WD1b (#613) — the same `||=` also settles absence against PRESENCE: the FIRST existence row for a
|
|
194
|
+
# path stands. Both orders are then conservative under a mid-run mutation, because the row that
|
|
195
|
+
# stands describes the world the earlier decision was shaped on, and it reads stale the moment the
|
|
196
|
+
# world stops matching it — a recompute, never a wrong hit.
|
|
197
|
+
def record_absence_entry(path)
|
|
198
|
+
entry = Cache::Descriptor::FileEntry.absent(path: path)
|
|
199
|
+
@mutex.synchronize { @file_entries[path] ||= entry }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# ADR-45 WD1b (#613) — the presence row: fresh while the probed path exists, stale once it is gone.
|
|
203
|
+
# It is the weakest row the boundary records (a rename that keeps a file at the path leaves it fresh),
|
|
204
|
+
# and it is the honest one for a probe that never read the bytes — a discovery root globbed for `.rb`
|
|
205
|
+
# files, a config file whose mere existence switched a mode. Ranked under a content row, and under an
|
|
206
|
+
# earlier existence row, by the `||=`/`=` split above.
|
|
207
|
+
def record_presence_entry(path)
|
|
208
|
+
entry = Cache::Descriptor::FileEntry.present(path: path)
|
|
209
|
+
@mutex.synchronize { @file_entries[path] ||= entry }
|
|
210
|
+
end
|
|
211
|
+
|
|
109
212
|
def record_url_entry(url, body)
|
|
110
213
|
digest = Digest::SHA256.hexdigest(body)
|
|
111
214
|
key = "url:#{url}"
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -76,6 +76,20 @@ module Rigor
|
|
|
76
76
|
File.file?(path) ? path : nil
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# The signature half of {.bundled_plugin_path}: the absolute `sig/` directory of the engine's OWN
|
|
80
|
+
# bundled copy of a plugin gem (`<ENGINE_ROOT>/plugins/<gem>/sig`), or nil when the engine does not
|
|
81
|
+
# bundle it, it ships no signatures, or the packaging carries no `plugins/` tree (ADR-27).
|
|
82
|
+
#
|
|
83
|
+
# Unlike its sibling this is NOT a require anchor — nothing loads Ruby from it. It exists so the
|
|
84
|
+
# engine can recognise its own bundled RBS when a project reaches that directory by a route the
|
|
85
|
+
# plugin registry never sees: a `signature_paths:` entry pointing straight at it (issue #672). The
|
|
86
|
+
# anchor is deliberately the same `ENGINE_ROOT`, so a git checkout and an installed `rigortype` gem
|
|
87
|
+
# answer identically and the two halves of a bundled plugin can never resolve to different copies.
|
|
88
|
+
def self.bundled_plugin_sig_path(gem_name)
|
|
89
|
+
path = File.join(ENGINE_ROOT, "plugins", gem_name, "sig")
|
|
90
|
+
File.directory?(path) ? path : nil
|
|
91
|
+
end
|
|
92
|
+
|
|
79
93
|
# @param entries [Array<String, Hash>] the raw `plugins:` list from the configuration.
|
|
80
94
|
# @return [Registry]
|
|
81
95
|
def load(entries)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analysis/crash_signature"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Protection
|
|
7
|
+
# Issue #686 — raised when an oracle's re-analysis came back from a crashed run, so its diagnostics say
|
|
8
|
+
# nothing about the code that was analysed.
|
|
9
|
+
#
|
|
10
|
+
# A named class, not a bare RuntimeError: {MutationScanner#classify} already folds a per-mutant
|
|
11
|
+
# `StandardError` into the `:harness_error` bucket, and the scanner's file-level rescue must catch THIS
|
|
12
|
+
# and nothing else, so the two failure modes stay distinguishable at every site that handles either.
|
|
13
|
+
class AnalyzerCrashed < StandardError; end
|
|
14
|
+
|
|
15
|
+
# The kill oracles' guard against judging a mutant by a run that never happened.
|
|
16
|
+
#
|
|
17
|
+
# A kill oracle answers one question: did re-analysing the mutant produce a diagnostic the clean
|
|
18
|
+
# baseline did not carry? When a check rule raises, `Runner#analyze_file_body` rescues it into ONE
|
|
19
|
+
# `internal analyzer error` diagnostic for the whole file and discards the rest — deterministically, on
|
|
20
|
+
# the SAME file, for the SAME reason. So the baseline and the mutant come back carrying the identical
|
|
21
|
+
# synthetic row, the set difference is empty, and the oracle reports the mutant SURVIVED. Not "we could
|
|
22
|
+
# not tell": survived. Every mutant in the affected file scores the same way, which inflates the
|
|
23
|
+
# survivor count — the mutation harness's headline signal — in the direction that manufactures work,
|
|
24
|
+
# and the harness has no way to notice.
|
|
25
|
+
#
|
|
26
|
+
# An indeterminate run must not be scored as either killed or survived, so the oracles raise here and
|
|
27
|
+
# let {MutationScanner} put the mutant in the `harness_errors` bucket (#264), which is already excluded
|
|
28
|
+
# from `killed + survived` and already surfaced by the CLI. A crash then READS as a crash.
|
|
29
|
+
#
|
|
30
|
+
# Only {Analysis::Result#crashed?} is armed — the check-rule rescue, the one shape that REPLACES a
|
|
31
|
+
# file's whole diagnostic list. Refusing more than that is the same error pointed the other way, and it
|
|
32
|
+
# was measured: on a project whose plugin `prepare` raises, refusing the `:plugin` row took a file from
|
|
33
|
+
# `killed=1 survived=6` to `killed=0 survived=0 harness_errors=7` — the builtin rules had run, their
|
|
34
|
+
# diagnostics were all present, and a real measurement was thrown away. `invoke_plugin_prepare` appends
|
|
35
|
+
# its row to every sequential run, so arming it would have refused every run for the life of the
|
|
36
|
+
# process. `:rbs_build` is excluded for the same reason plus one more: the site filter that admits
|
|
37
|
+
# mutations already drops receivers whose type did not resolve, so a class whose definition failed to
|
|
38
|
+
# build contributes no measured sites to begin with.
|
|
39
|
+
module AnalysisGuard
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# Takes the whole {Analysis::Result} rather than its diagnostics so the question is asked through
|
|
43
|
+
# {Analysis::Result#crashed?} — the same predicate the spec-side guard reads, off the same
|
|
44
|
+
# {Analysis::CrashSignature} table. Hands back the diagnostics, which is all a kill comparison wants.
|
|
45
|
+
#
|
|
46
|
+
# @param result [Rigor::Analysis::Result] one analysis run.
|
|
47
|
+
# @param context [String] which oracle call produced it, so the raise points at the right seam.
|
|
48
|
+
# @return [Array<Rigor::Analysis::Diagnostic>] the run's diagnostics, when the run was healthy.
|
|
49
|
+
# @raise [AnalyzerCrashed]
|
|
50
|
+
def checked(result, context:)
|
|
51
|
+
return result.diagnostics unless result.crashed?
|
|
52
|
+
|
|
53
|
+
raise AnalyzerCrashed,
|
|
54
|
+
"#{context}: the analyzer crashed, so this run's diagnostics say nothing about the code " \
|
|
55
|
+
"(#{Analysis::CrashSignature.describe(result.crash_diagnostics.first)}). A kill comparison " \
|
|
56
|
+
"against it would score the mutant a survivor it was never measured against. See issue #686."
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -5,6 +5,7 @@ require "tempfile"
|
|
|
5
5
|
require_relative "../analysis/buffer_binding"
|
|
6
6
|
require_relative "../analysis/runner"
|
|
7
7
|
require_relative "../inference/fork_map"
|
|
8
|
+
require_relative "analysis_guard"
|
|
8
9
|
require_relative "diagnostic_oracle"
|
|
9
10
|
require_relative "discovery_seed"
|
|
10
11
|
require_relative "kill_signature"
|
|
@@ -29,7 +30,7 @@ module Rigor
|
|
|
29
30
|
# closure, and the two effects were indistinguishable in the total.
|
|
30
31
|
#
|
|
31
32
|
# **The mutant's bytes are never on the measured file's disk.** For the closure half they are written to a
|
|
32
|
-
#
|
|
33
|
+
# block-scoped temp file and bound to the measured path through {Analysis::BufferBinding} — the #146
|
|
33
34
|
# editor seam, whose whole purpose is "analyse THESE bytes at THAT logical path". The binding reaches three
|
|
34
35
|
# places that would otherwise read the file as it sits on disk:
|
|
35
36
|
#
|
|
@@ -112,18 +113,6 @@ module Rigor
|
|
|
112
113
|
[path, *(@dependents[path] || [])]
|
|
113
114
|
end
|
|
114
115
|
|
|
115
|
-
# Release the process-private mutant file now, instead of waiting for `Tempfile`'s finalizer at process
|
|
116
|
-
# exit. A one-shot `rigor coverage` run does not need this — the process ends and the file goes with it —
|
|
117
|
-
# but a caller that outlives the oracle does: the spec suite's `after(:suite)` residue check (issue #330)
|
|
118
|
-
# runs while the process is still alive, so an un-released file reads as a leak there even though it
|
|
119
|
-
# would have been reclaimed a moment later. Idempotent, and safe on an oracle that never mutated
|
|
120
|
-
# anything (the file is created lazily).
|
|
121
|
-
def release!
|
|
122
|
-
@mutant_file&.close!
|
|
123
|
-
@mutant_file = nil
|
|
124
|
-
@mutant_pid = nil
|
|
125
|
-
end
|
|
126
|
-
|
|
127
116
|
private
|
|
128
117
|
|
|
129
118
|
# The diagnostic signatures the dependents of `path` report while `source` stands in for it. Empty (and
|
|
@@ -138,28 +127,35 @@ module Rigor
|
|
|
138
127
|
end
|
|
139
128
|
|
|
140
129
|
# Binds `source` to `path` for the duration of the block, yielding the binding and the seed tables
|
|
141
|
-
# rebuilt against it.
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# The process-private mutant file. Created lazily, and re-created after a fork ({CLI::MutationForkScan}
|
|
151
|
-
# workers must not share one path), which the pid guard detects.
|
|
130
|
+
# rebuilt against it.
|
|
131
|
+
#
|
|
132
|
+
# The mutant's bytes live in a **block-scoped** temp file: created and unlinked inside this method, so
|
|
133
|
+
# the oracle holds no file between calls and no caller needs a teardown hook to reclaim one. Issue #572
|
|
134
|
+
# — the previous design kept one `Tempfile` per process, which `Tempfile` reclaims only from its
|
|
135
|
+
# finalizer, i.e. at GC time. That made the file's disappearance a timing coin flip: the spec suite's
|
|
136
|
+
# issue-#330 residue check runs while the process is still alive, so it saw a live `rigor-mutant-*.rb`
|
|
137
|
+
# and failed a CI shard that the identical head passed on rerun.
|
|
152
138
|
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
139
|
+
# Per-mutant rather than per-process is safe on both axes the per-process comment defended. Freshness:
|
|
140
|
+
# the digest that invalidates the mutated file's discovery bundle is taken over the file's CONTENT and
|
|
141
|
+
# the bundles are keyed by the LOGICAL path, so nothing depends on the physical name being stable — a
|
|
142
|
+
# name that is never reused is strictly further from a stale answer than one rewritten in place. Cost:
|
|
143
|
+
# one `mkstemp` + `unlink` against the whole analysis-per-dependent this call already runs.
|
|
144
|
+
#
|
|
145
|
+
# The directory stays {Inference::ForkMap.child_scratch_dir}, and that is not redundant. A
|
|
146
|
+
# {CLI::MutationForkScan} worker unwinds this block on both paths it can take on its own — a normal
|
|
147
|
+
# return and the `StandardError` {Inference::ForkMap.run_worker} rescues — but a worker killed by a
|
|
148
|
+
# signal unwinds nothing, and its `exit!` runs neither `at_exit` nor a finalizer. The parent's fork
|
|
149
|
+
# tmpdir stays the backstop for exactly that case (issue #330); on the parent and on the sequential
|
|
150
|
+
# path the reader answers `nil`, which is `Tempfile.create`'s own default.
|
|
151
|
+
def with_mutant(source, path)
|
|
152
|
+
Tempfile.create(["rigor-mutant-", ".rb"], Inference::ForkMap.child_scratch_dir) do |file|
|
|
153
|
+
file.binmode
|
|
154
|
+
file.write(source)
|
|
155
|
+
file.flush
|
|
156
|
+
buffer = Analysis::BufferBinding.new(logical_path: path, physical_path: file.path)
|
|
157
|
+
yield(buffer, seed_for(buffer))
|
|
158
|
+
end
|
|
163
159
|
end
|
|
164
160
|
|
|
165
161
|
def seed_for(buffer)
|
|
@@ -179,11 +175,20 @@ module Rigor
|
|
|
179
175
|
# and emits nothing. A closure oracle built that way would report zero cross-file kills and look
|
|
180
176
|
# entirely plausible doing it. Passing both selects option B (#146), where the closure wins and the
|
|
181
177
|
# buffer is one member of it.
|
|
178
|
+
#
|
|
179
|
+
# Issue #686 — the guard is on this line for the reason the whole class exists. `killed?` decides by
|
|
180
|
+
# SET DIFFERENCE between the baseline's signatures and the mutant's, and a crashed check rule makes
|
|
181
|
+
# both sides the same deterministic `internal analyzer error` row on the same file: the difference is
|
|
182
|
+
# empty and the mutant is scored a survivor, not "indeterminate". Every mutant in the file scores the
|
|
183
|
+
# same way, which inflates the survivor count the harness reports as its headline signal.
|
|
182
184
|
def analyse(paths, buffer, seed)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
AnalysisGuard.checked(
|
|
186
|
+
Analysis::Runner.new(
|
|
187
|
+
configuration: @configuration, environment: @environment, prebuilt: @project_scan,
|
|
188
|
+
cache_store: nil, collect_stats: false, buffer: buffer, discovery_seed: seed, analyze_only: paths
|
|
189
|
+
).run(paths),
|
|
190
|
+
context: "ClosureKillOracle closure analysis of #{paths.join(', ')}"
|
|
191
|
+
)
|
|
187
192
|
end
|
|
188
193
|
end
|
|
189
194
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../analysis/runner"
|
|
4
|
+
require_relative "analysis_guard"
|
|
4
5
|
require_relative "kill_signature"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
@@ -42,11 +43,18 @@ module Rigor
|
|
|
42
43
|
|
|
43
44
|
private
|
|
44
45
|
|
|
46
|
+
# Issue #686 — the diagnostics of a CRASHED run are not an answer, and this oracle's whole job is to
|
|
47
|
+
# compare two runs' diagnostics. Both sides of that comparison come through here, so refusing once
|
|
48
|
+
# here covers `#baseline` and `#killed?` alike; {MutationScanner} turns the raise into a
|
|
49
|
+
# `harness_errors` count rather than a kill or a survivor.
|
|
45
50
|
def analyse(source, path)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
AnalysisGuard.checked(
|
|
52
|
+
Rigor::Analysis::Runner.new(
|
|
53
|
+
configuration: @configuration, environment: @environment, prebuilt: @project_scan,
|
|
54
|
+
cache_store: nil, collect_stats: false, discovery_seed: @discovery_seed
|
|
55
|
+
).run_source(source: source, path: path),
|
|
56
|
+
context: "DiagnosticOracle re-analysis of #{path}"
|
|
57
|
+
)
|
|
50
58
|
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
@@ -98,7 +98,7 @@ module Rigor
|
|
|
98
98
|
def_index = index.fetch(:def_index)
|
|
99
99
|
tables = { discovered_classes: index.fetch(:classes) }
|
|
100
100
|
%i[
|
|
101
|
-
def_nodes singleton_def_nodes def_sources singleton_def_sources superclasses includes
|
|
101
|
+
def_nodes def_nestings singleton_def_nodes def_sources singleton_def_sources superclasses includes
|
|
102
102
|
method_visibilities methods data_member_layouts struct_member_layouts
|
|
103
103
|
].each do |slot|
|
|
104
104
|
tables[seed_key(slot)] = def_index.fetch(slot)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Protection
|
|
5
|
+
# Issue #686 — the single definition of "was this file measured at all", and the one place the two
|
|
6
|
+
# counts' escalation rule is written down rather than restated.
|
|
7
|
+
#
|
|
8
|
+
# `killed + survived == 0` means two different things, and telling them apart is the whole point:
|
|
9
|
+
#
|
|
10
|
+
# - with no `harness_errors`, the file had no type-relevant mutation, and the vacuous-file convention
|
|
11
|
+
# scores it fully effective — there was no breakage available to miss;
|
|
12
|
+
# - with `harness_errors`, every mutant it had failed inside the harness, so the file measured the
|
|
13
|
+
# HARNESS and says nothing about the code. Borrowing the first's 1.0 made a wholly crashed file report
|
|
14
|
+
# 100% effective and pass `--threshold`.
|
|
15
|
+
#
|
|
16
|
+
# The predicate lives here because four call sites need the same answer — the per-file results, the exit
|
|
17
|
+
# gate, the stderr warning and both renderers — and four independent spellings of
|
|
18
|
+
# `harness_errors.positive? && total.zero?` agree only by coincidence (issue #696 review, second pass).
|
|
19
|
+
module MeasurementIntegrity
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] false only when nothing could be measured AND something failed trying.
|
|
23
|
+
def measured?(total:, harness_errors:)
|
|
24
|
+
!(total.zero? && harness_errors.positive?)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The report-level companion: a project ratio computed over nothing, where something went wrong.
|
|
28
|
+
# Distinct from an empty project, which is legitimately vacuous.
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def ratio_unmeasurable?(grand_total:, unmeasured_files:)
|
|
32
|
+
grand_total.zero? && unmeasured_files.positive?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "analysis_guard"
|
|
6
|
+
require_relative "measurement_integrity"
|
|
5
7
|
require_relative "mutator"
|
|
6
8
|
require_relative "diagnostic_oracle"
|
|
7
9
|
|
|
@@ -38,9 +40,21 @@ module Rigor
|
|
|
38
40
|
# harness-level failure measures the harness, not the code, exactly like a parse-invalid mutant).
|
|
39
41
|
def total = killed + survived
|
|
40
42
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
43
|
+
# Issue #686 — "nothing was measured here" is NOT the same as "there was nothing to measure", and
|
|
44
|
+
# `total.zero?` cannot tell them apart on its own. A file with no type-relevant mutation is
|
|
45
|
+
# vacuously fully effective; a file whose every mutant landed in `harness_errors` measured the
|
|
46
|
+
# harness and says nothing about the code. Before this, the second borrowed the first's 1.0 and a
|
|
47
|
+
# wholly crashed file reported 100% effective.
|
|
48
|
+
def measured? = MeasurementIntegrity.measured?(total: total, harness_errors: harness_errors)
|
|
49
|
+
|
|
50
|
+
# Effectiveness ratio. 0.0 rather than 1.0 for an unmeasured file — both are wrong as a measurement,
|
|
51
|
+
# and only one of them turns a red `--threshold` gate green. Read it next to {#measured?}, which is
|
|
52
|
+
# what the renderer and the exit gate consult.
|
|
53
|
+
def ratio
|
|
54
|
+
return 0.0 unless measured?
|
|
55
|
+
|
|
56
|
+
total.zero? ? 1.0 : killed.to_f / total
|
|
57
|
+
end
|
|
44
58
|
end
|
|
45
59
|
|
|
46
60
|
# ADR-70 — one type-survivor classified by the dynamic (test) axis. `protection` is `:test` (a test caught
|
|
@@ -62,8 +76,15 @@ module Rigor
|
|
|
62
76
|
def unprotected = sites.size
|
|
63
77
|
def total = type_killed + test_killed + unprotected
|
|
64
78
|
|
|
79
|
+
# Issue #686 — see {FileResult#measured?}.
|
|
80
|
+
def measured? = MeasurementIntegrity.measured?(total: total, harness_errors: harness_errors)
|
|
81
|
+
|
|
65
82
|
# Fused protected ratio — caught by *either* axis.
|
|
66
|
-
def ratio
|
|
83
|
+
def ratio
|
|
84
|
+
return 0.0 unless measured?
|
|
85
|
+
|
|
86
|
+
total.zero? ? 1.0 : (type_killed + test_killed).to_f / total
|
|
87
|
+
end
|
|
67
88
|
end
|
|
68
89
|
|
|
69
90
|
# @param configuration [Rigor::Configuration]
|
|
@@ -114,7 +135,9 @@ module Rigor
|
|
|
114
135
|
kept = kept_mutations(source, path)
|
|
115
136
|
return FileResult.new(path: path, killed: 0, survived: 0, sites: []) if kept.empty?
|
|
116
137
|
|
|
117
|
-
baseline =
|
|
138
|
+
baseline = clean_baseline(source, path)
|
|
139
|
+
return crashed_baseline_result(path, kept.size) if baseline.nil?
|
|
140
|
+
|
|
118
141
|
killed = 0
|
|
119
142
|
harness_errors = 0
|
|
120
143
|
sites = []
|
|
@@ -140,7 +163,12 @@ module Rigor
|
|
|
140
163
|
kept = kept_mutations(source, path)
|
|
141
164
|
return FusedFileResult.new(path: path, type_killed: 0, test_killed: 0, sites: []) if kept.empty?
|
|
142
165
|
|
|
143
|
-
baseline =
|
|
166
|
+
baseline = clean_baseline(source, path)
|
|
167
|
+
if baseline.nil?
|
|
168
|
+
return FusedFileResult.new(path: path, type_killed: 0, test_killed: 0, sites: [],
|
|
169
|
+
harness_errors: kept.size)
|
|
170
|
+
end
|
|
171
|
+
|
|
144
172
|
type_killed = 0
|
|
145
173
|
test_killed = 0
|
|
146
174
|
harness_errors = 0
|
|
@@ -185,6 +213,12 @@ module Rigor
|
|
|
185
213
|
# only the latter is a harness defect worth counting and surfacing. Both stay OUT of `killed + survived`
|
|
186
214
|
# exactly as before (containment is unchanged) — #264 is about visibility, not about admitting either
|
|
187
215
|
# bucket into the denominator.
|
|
216
|
+
#
|
|
217
|
+
# #686 — a {AnalysisGuard::AnalyzerCrashed} from the oracle's re-analysis arrives here as a
|
|
218
|
+
# `StandardError` and lands in the same bucket, which is exactly right: an indeterminate mutant must
|
|
219
|
+
# be scored neither killed nor survived. Before the oracle refused, a crashed re-analysis produced the
|
|
220
|
+
# same synthetic diagnostic on both sides of the kill comparison, so the mutant was scored a SURVIVOR
|
|
221
|
+
# and the harness reported a hole it had never measured.
|
|
188
222
|
def classify(source, path, mut, baseline)
|
|
189
223
|
mutant_source = mut.apply(source)
|
|
190
224
|
return :invalid unless Prism.parse(mutant_source).success?
|
|
@@ -196,6 +230,27 @@ module Rigor
|
|
|
196
230
|
:harness_error
|
|
197
231
|
end
|
|
198
232
|
|
|
233
|
+
# The clean per-file baseline, or nil when the analyzer crashed while computing it (#686).
|
|
234
|
+
#
|
|
235
|
+
# The baseline is computed ONCE per file, outside {#classify}'s per-mutant rescue, so a crash here is
|
|
236
|
+
# not one mutant's problem: every mutant of this file would be judged against a baseline that knows
|
|
237
|
+
# nothing, and each would come back a survivor. Reported as `harness_errors` — one per mutant that
|
|
238
|
+
# would have been measured — rather than raised, so one bad file does not abort a whole sweep, and
|
|
239
|
+
# rather than silently skipped, so the count the CLI already warns about accounts for it.
|
|
240
|
+
#
|
|
241
|
+
# Deliberately narrow: only {AnalyzerCrashed}. Any other exception out of an oracle's baseline
|
|
242
|
+
# propagates exactly as it did before, because widening that rescue would hide unrelated harness bugs
|
|
243
|
+
# behind the very bucket this issue exists to make honest.
|
|
244
|
+
def clean_baseline(source, path)
|
|
245
|
+
@oracle.baseline(source: source, path: path)
|
|
246
|
+
rescue AnalyzerCrashed
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def crashed_baseline_result(path, mutant_count)
|
|
251
|
+
FileResult.new(path: path, killed: 0, survived: 0, sites: [], harness_errors: mutant_count)
|
|
252
|
+
end
|
|
253
|
+
|
|
199
254
|
def sample(mutations)
|
|
200
255
|
return mutations unless @limit
|
|
201
256
|
|