rigortype 0.3.6 → 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 +1 -1
- 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 +409 -11
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- 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/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 +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- 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 +147 -10
- 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/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/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- 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.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -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/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/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
- metadata +69 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# Issues #665 / #674 / #683 / #686 / #696 — the ONE definition of "an internal failure made this run
|
|
6
|
+
# report less than it should have".
|
|
7
|
+
#
|
|
8
|
+
# Every member of that family shares a defect: the run still exits 0 and its diagnostic list still
|
|
9
|
+
# looks like an answer, so an absence assertion (`not_to include(...)`, `be_empty`, `all(eq(...))`)
|
|
10
|
+
# holds on it and a comparison of two such runs (`baseline == mutant`, `recheck == full_run`) reports
|
|
11
|
+
# "no difference" — because both sides carry the same synthetic row, not because the code agreed. Four
|
|
12
|
+
# separate consumers needed to recognise these shapes, and before this module each recognised them by
|
|
13
|
+
# matching a diagnostic MESSAGE with its own string literal. A fourth copy is what this exists to stop:
|
|
14
|
+
# a reworded message silently disarms whichever copies were not updated, and nothing goes red.
|
|
15
|
+
#
|
|
16
|
+
# ## The three shapes
|
|
17
|
+
#
|
|
18
|
+
# - `:check_rule` — message begins `internal analyzer error`, `rule: nil`. Built by
|
|
19
|
+
# `Runner#analyze_file_body` / `WorkerSession#analyze_body`'s `rescue StandardError`.
|
|
20
|
+
# - `:plugin` — `severity: :error`, `source_family: :plugin_loader`, `rule: "runtime-error"`. Built by
|
|
21
|
+
# `Runner#collect_plugin_diagnostics` / `Runner::ProjectPrePasses#invoke_plugin_prepare`.
|
|
22
|
+
# - `:rbs_build` — `rule` in {RBS_BUILD_FAILURE_RULES}. Recorded by `Environment::RbsLoader`'s
|
|
23
|
+
# env-build and definition-build rescues, surfaced by {Runner::DiagnosticAggregator}.
|
|
24
|
+
#
|
|
25
|
+
# The `:plugin` shape is keyed on the structured `(severity, source_family, rule)` triple rather than on
|
|
26
|
+
# the bare rule name: a plugin is free to define its OWN `"runtime-error"` (or `"load-error"`) under its
|
|
27
|
+
# own `source_family: "plugin.<id>"`, and several specs legitimately assert on one. Only the
|
|
28
|
+
# `:plugin_loader` family paired with `"runtime-error"` is the isolation envelope's own row.
|
|
29
|
+
#
|
|
30
|
+
# ## Only ONE shape discards a file's analysis
|
|
31
|
+
#
|
|
32
|
+
# {.discards_file_analysis?} answers true for `:check_rule` alone, and the distinction is not pedantry —
|
|
33
|
+
# it decides whether a consumer may still read the run's diagnostics.
|
|
34
|
+
#
|
|
35
|
+
# - `:check_rule` — `Runner#analyze_file_body` rescues around the WHOLE per-file body, so what comes
|
|
36
|
+
# back is `[crash_row]` and every diagnostic that file would have produced is gone. Nothing is left to
|
|
37
|
+
# read.
|
|
38
|
+
# - `:plugin` — `collect_plugin_diagnostics` replaces only the raising PLUGIN's contribution;
|
|
39
|
+
# `CheckRules.diagnose` has already returned, and `invoke_plugin_prepare` adds one row at `.rigor.yml`
|
|
40
|
+
# and lets the run proceed. The builtin rules ran, and their diagnostics are all still there. A
|
|
41
|
+
# consumer that refuses the whole run over this throws away a real measurement — and because the
|
|
42
|
+
# prepare row is appended to every sequential run, it would refuse every run for the life of the
|
|
43
|
+
# process.
|
|
44
|
+
# - `:rbs_build` — the analysis ran to completion and every rule fired, over a type universe missing one
|
|
45
|
+
# class (`rbs.coverage.definition-build-failed`) or all of them
|
|
46
|
+
# (`rbs.coverage.environment-build-failed`). A degradation the user causes and the diagnostic itself
|
|
47
|
+
# reports; a project can sit on it for a release while it fixes its `sig/`.
|
|
48
|
+
#
|
|
49
|
+
# The two consumer tiers therefore differ on purpose. The ADR-69 kill oracles arm `:check_rule` only:
|
|
50
|
+
# refusing a run they could still have measured is the same "manufactures work" error as scoring an
|
|
51
|
+
# unmeasured mutant a survivor, pointed the other way (issue #686 review). The spec harness additionally
|
|
52
|
+
# arms `:plugin`, which is suite POLICY rather than a claim about the diagnostics — no spec has a reason
|
|
53
|
+
# to want a plugin crashing under it, and `allow_plugin_crash:` is the opt-out for the handful whose
|
|
54
|
+
# subject IS the isolation envelope.
|
|
55
|
+
module CrashSignature
|
|
56
|
+
# The `rescue StandardError` in `Runner#analyze_file_body` / `WorkerSession#analyze_body` folds a
|
|
57
|
+
# raising check rule (or a plugin's node-rule contribution) into ONE diagnostic per file with this
|
|
58
|
+
# message prefix. Matched on the prefix alone, exactly as the pre-#696 guards did: adding a `rule.nil?`
|
|
59
|
+
# conjunct would NARROW the match, and the whole point of a guard against a bug is that it stays armed
|
|
60
|
+
# when the shape shifts.
|
|
61
|
+
CHECK_RULE_MESSAGE_PREFIX = "internal analyzer error"
|
|
62
|
+
|
|
63
|
+
PLUGIN_SOURCE_FAMILY = :plugin_loader
|
|
64
|
+
PLUGIN_RULE = "runtime-error"
|
|
65
|
+
|
|
66
|
+
# The `rbs.coverage.*` rules that mean declared types went missing from this run — an env-wide
|
|
67
|
+
# collapse and its per-class sibling (#696). Ordered widest consequence last, the way the two rows sit
|
|
68
|
+
# in `docs/type-specification/diagnostic-policy.md`.
|
|
69
|
+
RBS_BUILD_FAILURE_RULES = %w[
|
|
70
|
+
rbs.coverage.definition-build-failed
|
|
71
|
+
rbs.coverage.environment-build-failed
|
|
72
|
+
].freeze
|
|
73
|
+
|
|
74
|
+
# The one reason that means a file's whole diagnostic list was replaced by a crash row. `:plugin` and
|
|
75
|
+
# `:rbs_build` both leave a readable run behind — see the class doc.
|
|
76
|
+
DISCARDS_FILE_ANALYSIS_REASON = :check_rule
|
|
77
|
+
|
|
78
|
+
module_function
|
|
79
|
+
|
|
80
|
+
# @param diagnostic [Rigor::Analysis::Diagnostic]
|
|
81
|
+
# @return [Symbol, nil] `:check_rule`, `:plugin`, `:rbs_build`, or nil for an ordinary diagnostic.
|
|
82
|
+
def reason(diagnostic)
|
|
83
|
+
return :check_rule if diagnostic.message.to_s.start_with?(CHECK_RULE_MESSAGE_PREFIX)
|
|
84
|
+
return :plugin if plugin_isolation_row?(diagnostic)
|
|
85
|
+
return :rbs_build if RBS_BUILD_FAILURE_RULES.include?(diagnostic.rule)
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# True when `diagnostic` is the rescue row that REPLACED a file's analysis — the only shape after which
|
|
91
|
+
# the run's diagnostics say nothing about the code. NOT a general "something went wrong" predicate; see
|
|
92
|
+
# the class doc for why `:plugin` and `:rbs_build` are excluded.
|
|
93
|
+
#
|
|
94
|
+
# @param diagnostic [Rigor::Analysis::Diagnostic]
|
|
95
|
+
def discards_file_analysis?(diagnostic)
|
|
96
|
+
reason(diagnostic) == DISCARDS_FILE_ANALYSIS_REASON
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A one-line "<reason> at <path>:<line>: <message>" for a failure message, so whoever reads the raise
|
|
100
|
+
# sees which shape fired and where without re-deriving it.
|
|
101
|
+
#
|
|
102
|
+
# @param diagnostic [Rigor::Analysis::Diagnostic]
|
|
103
|
+
# @return [String]
|
|
104
|
+
def describe(diagnostic)
|
|
105
|
+
"#{reason(diagnostic) || :unknown} at #{diagnostic.path}:#{diagnostic.line}: #{diagnostic.message}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def plugin_isolation_row?(diagnostic)
|
|
109
|
+
diagnostic.severity == :error &&
|
|
110
|
+
diagnostic.source_family == PLUGIN_SOURCE_FAMILY &&
|
|
111
|
+
diagnostic.rule == PLUGIN_RULE
|
|
112
|
+
end
|
|
113
|
+
private_class_method :plugin_isolation_row?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -168,9 +168,17 @@ module Rigor
|
|
|
168
168
|
accumulate_read(accumulator, path, symbol)
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
-
# Records a cross-file
|
|
172
|
-
#
|
|
173
|
-
|
|
171
|
+
# Records a NAME-KEYED cross-file dependency: this consumer's answer depends on what the project makes
|
|
172
|
+
# of `name` under `kind`, so a recheck must re-analyze it when that changes. The recorded key is
|
|
173
|
+
# inverted into `negative_dependents` and matched against the producer for its kind.
|
|
174
|
+
#
|
|
175
|
+
# For `:method` / `:class` / `:toplevel` this is recorded only on a MISS ({read_missing}) — a resolved
|
|
176
|
+
# method or class already carries a positive edge to its definition site, which covers every later
|
|
177
|
+
# change to it. Issue #644's `:constant` kind is recorded on a HIT as well, because a constant's
|
|
178
|
+
# published value is a function of the WHOLE project's write set for the name, not of the one file that
|
|
179
|
+
# currently wins: a second declarer appearing in a file the reader has no edge to must drop the name to
|
|
180
|
+
# `Dynamic` at that reader, and only a name-keyed edge can see it.
|
|
181
|
+
def read_name(kind, name)
|
|
174
182
|
accumulator = Thread.current[KEY]
|
|
175
183
|
return if accumulator.nil?
|
|
176
184
|
|
|
@@ -179,6 +187,13 @@ module Rigor
|
|
|
179
187
|
accumulator.missing.add(entry)
|
|
180
188
|
end
|
|
181
189
|
|
|
190
|
+
# The miss-only spelling of {read_name}: a cross-file lookup of `name` (kind `:method` / `:class` /
|
|
191
|
+
# `:toplevel` / …) that resolved to nothing. Kept as the name the internal spec's negative-edge
|
|
192
|
+
# contract uses.
|
|
193
|
+
def read_missing(kind, name)
|
|
194
|
+
read_name(kind, name)
|
|
195
|
+
end
|
|
196
|
+
|
|
182
197
|
# The positive-edge aggregation shared by {read_site} and {replay}.
|
|
183
198
|
def accumulate_read(accumulator, path, symbol)
|
|
184
199
|
accumulator.sources << path
|
|
@@ -139,7 +139,8 @@ module Rigor
|
|
|
139
139
|
def descend_class_or_module(node, qualified_prefix, in_singleton_class, accumulator, budget)
|
|
140
140
|
name = Source::ConstantPath.qualified_name_or_nil(node.constant_path)
|
|
141
141
|
if name && node.body
|
|
142
|
-
|
|
142
|
+
child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
143
|
+
walk_node(node.body, child_prefix, in_singleton_class, accumulator, budget)
|
|
143
144
|
else
|
|
144
145
|
walk_children(node, qualified_prefix, in_singleton_class, accumulator, budget)
|
|
145
146
|
end
|
|
@@ -120,6 +120,34 @@ module Rigor
|
|
|
120
120
|
appeared.freeze
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# Issue #644 — the qualified constant names whose PUBLICATION could have moved in this edit: the names
|
|
124
|
+
# whose per-file census descriptor differs between the before- and after-state of any file in `paths`.
|
|
125
|
+
# The value-constant twin of {appeared_classes}, and deliberately NOT its analogue — a class either
|
|
126
|
+
# exists or does not, whereas a constant's published answer is a function of the whole project's write
|
|
127
|
+
# set for the name, so a *name* diff is the wrong question. All four ways the answer moves are a
|
|
128
|
+
# descriptor change and none of them is an appearance:
|
|
129
|
+
#
|
|
130
|
+
# - the value edited (`[50]` -> `[7]`),
|
|
131
|
+
# - the write becoming unpublishable or publishable again (`[50]` <-> `:unpublishable`),
|
|
132
|
+
# - a SECOND declarer appearing, which drops the name to `Dynamic` (`{}` -> `{NAME => …}` in that file),
|
|
133
|
+
# - a second declarer going away, which restores the precise answer.
|
|
134
|
+
#
|
|
135
|
+
# `paths` MUST therefore include removed files as well as changed and added ones; a removed file's
|
|
136
|
+
# after-state is simply absent, so its whole before-census counts as changed. Returns a frozen Set of
|
|
137
|
+
# qualified-name Strings. `before` / `after` map a path to `{name => descriptor}`.
|
|
138
|
+
def changed_constant_publications(paths, before, after)
|
|
139
|
+
moved = Set.new
|
|
140
|
+
paths.each do |path|
|
|
141
|
+
was = before[path] || {}
|
|
142
|
+
now = after[path] || {}
|
|
143
|
+
# `eql?`, not `==`: `[1] == [1.0]` is TRUE, so `LIMIT = 1` becoming `LIMIT = 1.0` would move the
|
|
144
|
+
# published type (`Constant[1]` -> `Constant[1.0]`) while this diff called it unchanged. The
|
|
145
|
+
# declaration signature backstops that case today; the producer must not depend on it.
|
|
146
|
+
(was.keys | now.keys).each { |name| moved << name unless was[name].eql?(now[name]) }
|
|
147
|
+
end
|
|
148
|
+
moved.freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
123
151
|
# ADR-46 slice 3 — the consumers to re-check because a name they looked up and *missed* (a negative
|
|
124
152
|
# dependency) now resolves. `keys` is the set of negative-dependency keys (`"toplevel:foo"` /
|
|
125
153
|
# `"method:C#m"`) the appeared symbols would satisfy; `negative_dependents` maps each key to the Set of
|
|
@@ -98,6 +98,10 @@ module Rigor
|
|
|
98
98
|
@missing = {} # consumer => Set<"kind:name"> it looked up and missed
|
|
99
99
|
@negative_dependents = {} # "kind:name" => Set<consumer> (inverted @missing)
|
|
100
100
|
@class_decls = {} # path => Set<qualified class name declared in the file>
|
|
101
|
+
# Issue #644 — the value-constant twin: path => { qualified constant name => publication descriptor }.
|
|
102
|
+
# Diffed on a recheck to find every constant whose PUBLISHED answer could have moved, which satisfies
|
|
103
|
+
# the `constant:` edges a reader records at every reference, resolved or not.
|
|
104
|
+
@constant_decls = {}
|
|
101
105
|
# ADR-89 WD2 — per-def observed-key return summaries: [path, symbol] => { keys:, returns:, effects: }.
|
|
102
106
|
# Harvested from the ADR-84 return memo after each run; drives the behavioural-stability gate.
|
|
103
107
|
@return_summaries = {}
|
|
@@ -259,6 +263,7 @@ module Rigor
|
|
|
259
263
|
declaration_signatures = (summary && summary[:declaration_signatures]) || {}
|
|
260
264
|
new_fps = symbol_fingerprints_from_index(scan_index)
|
|
261
265
|
new_class_decls = class_declarations_from_index(scan_index)
|
|
266
|
+
new_constant_decls = constant_declarations_from_index(scan_index)
|
|
262
267
|
changed_pairs = Incremental.changed_symbol_pairs(changed, @symbol_fingerprints, new_fps)
|
|
263
268
|
# ADR-89 WD1 — a changed file whose DECLARATION signature (per-def parameter shape / visibility /
|
|
264
269
|
# ancestry / member layout / def line, bodies excluded) is byte-identical to the snapshot is
|
|
@@ -275,7 +280,8 @@ module Rigor
|
|
|
275
280
|
# minus those stable pairs, and only it (not `changed_pairs`) drives the symbol-dependent fan-out.
|
|
276
281
|
symbol_pairs = behaviourally_unstable_pairs(changed_pairs, unstable, scan_index) | param_pairs
|
|
277
282
|
base = dependents_base(unstable, symbol_pairs)
|
|
278
|
-
closure = base | changed.to_set | added.to_set |
|
|
283
|
+
closure = base | changed.to_set | added.to_set |
|
|
284
|
+
negative_affected(scan, removed, new_fps, new_class_decls, new_constant_decls)
|
|
279
285
|
closure = param_seed_closure(closure, param_files)
|
|
280
286
|
removed.each { |path| closure |= @dependents[path] || Set.new }
|
|
281
287
|
closure.freeze
|
|
@@ -550,12 +556,7 @@ module Rigor
|
|
|
550
556
|
@symbol_sources = payload.symbol_sources || {}
|
|
551
557
|
@ancestry_sources = payload.ancestry_sources || {}
|
|
552
558
|
@symbol_fingerprints = payload.symbol_fingerprints || {}
|
|
553
|
-
|
|
554
|
-
# only loses the appeared-symbol re-check refinement; such a snapshot is never loaded by a slice-3+
|
|
555
|
-
# engine because the engine version + schema is part of the snapshot fingerprint, and
|
|
556
|
-
# `--verify-incremental` backstops any residual under-capture, so it is never unsound).
|
|
557
|
-
@missing = payload.missing || {}
|
|
558
|
-
@class_decls = payload.class_decls || {}
|
|
559
|
+
restore_structural_declarations(payload)
|
|
559
560
|
@return_summaries = payload.return_summaries || {}
|
|
560
561
|
@param_table = payload.param_table || {} # ADR-67 WD6c lift — the seeds the cache was built under.
|
|
561
562
|
# ADR-103 WD13 / #382 — the effects sidecar rides its own identity, so a snapshot whose summaries
|
|
@@ -569,12 +570,24 @@ module Rigor
|
|
|
569
570
|
@negative_dependents = Incremental.invert(@missing)
|
|
570
571
|
end
|
|
571
572
|
|
|
573
|
+
# ADR-46 slice 3 / issue #644 — the structural tier's negative-edge state: the per-consumer missed
|
|
574
|
+
# lookups plus the two per-file DECLARATION sets (`class_decls`, `constant_decls`) whose diff says what
|
|
575
|
+
# APPEARED in an edit. Absent in an older snapshot → empty, which only loses the appeared-symbol
|
|
576
|
+
# refinement; such a snapshot is never loaded by a newer engine (the version + SCHEMA are part of the
|
|
577
|
+
# fingerprint) and `--verify-incremental` backstops any residual under-capture. Extracted to keep
|
|
578
|
+
# {#restore} under its ABC budget.
|
|
579
|
+
def restore_structural_declarations(payload)
|
|
580
|
+
@missing = payload.missing || {}
|
|
581
|
+
@class_decls = payload.class_decls || {}
|
|
582
|
+
@constant_decls = payload.constant_decls || {}
|
|
583
|
+
end
|
|
584
|
+
|
|
572
585
|
def to_payload
|
|
573
586
|
Cache::IncrementalSnapshot::Payload.new(
|
|
574
587
|
cache: @cache, sources: @sources, digests: @digests, analyzed: @analyzed,
|
|
575
588
|
symbol_sources: @symbol_sources, ancestry_sources: @ancestry_sources,
|
|
576
589
|
symbol_fingerprints: @symbol_fingerprints, missing: @missing,
|
|
577
|
-
class_decls: @class_decls, seed_bundles: @seed_bundles,
|
|
590
|
+
class_decls: @class_decls, constant_decls: @constant_decls, seed_bundles: @seed_bundles,
|
|
578
591
|
plugin_fact_digest: @plugin_fact_digest,
|
|
579
592
|
return_summaries: marshal_safe_return_summaries,
|
|
580
593
|
param_table: marshal_safe_param_table,
|
|
@@ -754,6 +767,7 @@ module Rigor
|
|
|
754
767
|
# Wholesale replace (the subset runner's pre-pass is complete): a file that lost its last class must
|
|
755
768
|
# drop out of the map so a later re-add registers as an appearance.
|
|
756
769
|
@class_decls = runner.class_declarations
|
|
770
|
+
@constant_decls = runner.constant_declarations
|
|
757
771
|
end
|
|
758
772
|
|
|
759
773
|
# Per-symbol body fingerprints from the pre-parsed `index` (the shared scan-summary def-index — Prism
|
|
@@ -792,11 +806,18 @@ module Rigor
|
|
|
792
806
|
# a prior missed lookup. Maps each appeared `"ClassName#method"` to the negative-dependency key it
|
|
793
807
|
# would satisfy (`toplevel:foo` for a top-level def, `method:C#m` otherwise), then unions the recorded
|
|
794
808
|
# negative-dependents of those keys.
|
|
795
|
-
def negative_affected(changed, new_fingerprints, new_class_decls)
|
|
809
|
+
def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls)
|
|
796
810
|
appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
|
|
797
811
|
appeared_classes = Incremental.appeared_classes(changed, @class_decls, new_class_decls)
|
|
812
|
+
# Issue #644 — a constant whose PUBLICATION moved satisfies the `constant:` kind, keyed on the same
|
|
813
|
+
# last segment the consumer recorded. REMOVED files join the diff here and nowhere else: deleting a
|
|
814
|
+
# second declarer restores the precise answer, and no other producer input can see that.
|
|
815
|
+
moved_constants = Incremental.changed_constant_publications(
|
|
816
|
+
changed + removed, @constant_decls, new_constant_decls
|
|
817
|
+
)
|
|
798
818
|
keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
|
|
799
819
|
keys.concat(appeared_classes.map { |klass| "class:#{klass.split('::').last}" })
|
|
820
|
+
keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
|
|
800
821
|
Incremental.negative_closure(keys, @negative_dependents)
|
|
801
822
|
end
|
|
802
823
|
|
|
@@ -812,6 +833,20 @@ module Rigor
|
|
|
812
833
|
result.transform_values(&:freeze).freeze
|
|
813
834
|
end
|
|
814
835
|
|
|
836
|
+
# Issue #644 — the per-file PUBLICATION CENSUS of the pre-parsed `index`:
|
|
837
|
+
# `{ path => { constant name => descriptor } }`, the twin of {#class_declarations_from_index}. The
|
|
838
|
+
# descriptor is what the diff compares — see {Incremental.changed_constant_publications} for why a bare
|
|
839
|
+
# name set cannot answer the question.
|
|
840
|
+
def constant_declarations_from_index(index)
|
|
841
|
+
return {} if index.nil?
|
|
842
|
+
|
|
843
|
+
result = Hash.new { |hash, key| hash[key] = {} }
|
|
844
|
+
index[:constant_writes].each do |name, by_path|
|
|
845
|
+
by_path.each { |path, descriptor| result[path][name] = descriptor }
|
|
846
|
+
end
|
|
847
|
+
result.transform_values(&:freeze).freeze
|
|
848
|
+
end
|
|
849
|
+
|
|
815
850
|
# ADR-89 WD2 — the (live, freshly parsed) def node for a `"Class#method"` / `"Class.method"` symbol in
|
|
816
851
|
# the changed-set scan index, or nil. Instance methods live under `def_nodes`, singleton under
|
|
817
852
|
# `singleton_def_nodes`, both keyed by class name then method Symbol.
|
|
@@ -151,7 +151,7 @@ module Rigor
|
|
|
151
151
|
# value actually moves (a new action, a changed sig), which is what a cached diagnostic can depend on;
|
|
152
152
|
# a value-preserving edit keeps the recheck incremental. Returns true when the plugin declared at least
|
|
153
153
|
# one producer (a surface); a producer whose value cannot be digested marks the plugin opaque.
|
|
154
|
-
# rubocop:disable Naming/PredicateMethod -- reports has-surface AND appends parts (a side-effecting builder)
|
|
154
|
+
# rubocop:disable-next Naming/PredicateMethod -- reports has-surface AND appends parts (a side-effecting builder)
|
|
155
155
|
def collect_producer_parts(plugin, id, parts, opaque)
|
|
156
156
|
producer_ids = plugin.class.producers.keys
|
|
157
157
|
return false if producer_ids.empty?
|
|
@@ -163,7 +163,6 @@ module Rigor
|
|
|
163
163
|
end
|
|
164
164
|
true
|
|
165
165
|
end
|
|
166
|
-
# rubocop:enable Naming/PredicateMethod
|
|
167
166
|
|
|
168
167
|
# Channel (c): the optional {Plugin::Base#incremental_state_fingerprint} hook. Returns true when the
|
|
169
168
|
# plugin defines it (it has a surface).
|
|
@@ -81,7 +81,8 @@ module Rigor
|
|
|
81
81
|
next if use.name.nil?
|
|
82
82
|
|
|
83
83
|
Scan::Reference.new(as_written: use.name.sub(/\A::/, ""), nesting: [].freeze, from: nil,
|
|
84
|
-
role: :production, path: use.path, line: use.line
|
|
84
|
+
role: :production, path: use.path, line: use.line,
|
|
85
|
+
rooted: use.name.start_with?("::"))
|
|
85
86
|
end
|
|
86
87
|
end
|
|
87
88
|
|
|
@@ -148,7 +149,7 @@ module Rigor
|
|
|
148
149
|
@references.each do |ref|
|
|
149
150
|
next unless yield(ref)
|
|
150
151
|
|
|
151
|
-
target = resolve(ref.as_written, ref.nesting)
|
|
152
|
+
target = resolve(ref.as_written, ref.nesting, rooted: ref.rooted)
|
|
152
153
|
set << target if target && @owned.include?(target)
|
|
153
154
|
end
|
|
154
155
|
set
|
|
@@ -157,7 +158,7 @@ module Rigor
|
|
|
157
158
|
# `[from_fqn_or_nil, to_fqn, role]` for every reference that resolves to an owned declaration.
|
|
158
159
|
def resolved_edges
|
|
159
160
|
@resolved_edges ||= @references.filter_map do |ref|
|
|
160
|
-
target = resolve(ref.as_written, ref.nesting)
|
|
161
|
+
target = resolve(ref.as_written, ref.nesting, rooted: ref.rooted)
|
|
161
162
|
next unless target && @owned.include?(target)
|
|
162
163
|
next if ref.from == target # a declaration referencing itself is not evidence of use
|
|
163
164
|
|
|
@@ -197,8 +198,16 @@ module Rigor
|
|
|
197
198
|
end
|
|
198
199
|
|
|
199
200
|
# Ruby's constant lookup at name granularity: `Module.nesting` innermost first, then the ancestors of
|
|
200
|
-
# the innermost cresting scope (#354), then the bare name.
|
|
201
|
-
|
|
201
|
+
# the innermost cresting scope (#354), then the bare name. When `rooted: true`, lexical nesting and
|
|
202
|
+
# ancestors are skipped, answering the top-level declaration directly (#625).
|
|
203
|
+
def resolve(as_written, nesting, rooted: false)
|
|
204
|
+
if rooted
|
|
205
|
+
return as_written if @by_fqn.key?(as_written)
|
|
206
|
+
|
|
207
|
+
idx = as_written.rindex("::")
|
|
208
|
+
return idx ? resolve(as_written[0, idx], nesting, rooted: true) : nil
|
|
209
|
+
end
|
|
210
|
+
|
|
202
211
|
walker = nesting.dup
|
|
203
212
|
until walker.empty?
|
|
204
213
|
candidate = (walker + [as_written]).join("::")
|
|
@@ -28,8 +28,11 @@ module Rigor
|
|
|
28
28
|
|
|
29
29
|
# One constant reference. `from` is the fully-qualified name of the innermost enclosing declaration, or
|
|
30
30
|
# nil for a reference written at file level — that is what makes the graph a reachability graph rather
|
|
31
|
-
# than a reference count (ADR-102 WD2). `role` is the referring FILE's role (WD8).
|
|
32
|
-
|
|
31
|
+
# than a reference count (ADR-102 WD2). `role` is the referring FILE's role (WD8). `rooted` is true
|
|
32
|
+
# when the constant was written with a leading `::` (#625).
|
|
33
|
+
Reference = Data.define(:as_written, :nesting, :from, :role, :path, :line, :rooted) do
|
|
34
|
+
def initialize(rooted: false, **) = super
|
|
35
|
+
end
|
|
33
36
|
|
|
34
37
|
# ADR-102 WD4 — a site where a constant is reached by a mechanism the static reading cannot follow.
|
|
35
38
|
# `name` is the exact constant when the argument is a literal (`"Foo".constantize`), in which case this
|
|
@@ -248,7 +251,8 @@ module Rigor
|
|
|
248
251
|
|
|
249
252
|
@references << Reference.new(as_written: as_written, nesting: nesting.dup.freeze,
|
|
250
253
|
from: nesting.empty? ? nil : nesting.join("::"),
|
|
251
|
-
role: @role, path: @path, line: node.location.start_line
|
|
254
|
+
role: @role, path: @path, line: node.location.start_line,
|
|
255
|
+
rooted: Source::ConstantPath.rooted?(node))
|
|
252
256
|
end
|
|
253
257
|
end
|
|
254
258
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "crash_signature"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Analysis
|
|
5
7
|
class Result
|
|
@@ -17,6 +19,36 @@ module Rigor
|
|
|
17
19
|
diagnostics.none?(&:error?)
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
# Issue #686 — true when a check rule (or a plugin's node-rule contribution) raised, so
|
|
23
|
+
# `Runner#analyze_file_body`'s rescue REPLACED the affected file's whole diagnostic list with one
|
|
24
|
+
# synthetic row. What comes back is then not a weaker account of the code; it is no account of it.
|
|
25
|
+
#
|
|
26
|
+
# Any consumer that compares two results, or reads an EMPTY result as "clean", has to ask this first.
|
|
27
|
+
# A crashed baseline and a crashed mutant carry the same synthetic row, so `==` reports agreement; an
|
|
28
|
+
# absence assertion holds on the one-diagnostic list the rescue leaves behind. That is how a kill
|
|
29
|
+
# oracle scored survivors it never measured (#686) and how 1,177 spec examples passed with every check
|
|
30
|
+
# rule crashing (#674).
|
|
31
|
+
#
|
|
32
|
+
# Narrower than "the run reported a failure", deliberately. A raising PLUGIN loses only that plugin's
|
|
33
|
+
# contribution and a failed RBS definition build loses only a type — in both cases every builtin rule
|
|
34
|
+
# still ran and its diagnostics are still here, so a consumer that refused those would be discarding a
|
|
35
|
+
# real measurement. {CrashSignature} carries the full table and the reasoning.
|
|
36
|
+
#
|
|
37
|
+
# `success?` is deliberately NOT this question: a crash makes `success?` false (the row is `:error`),
|
|
38
|
+
# which is why the CLI is loud and CI goes red. It is the callers that read the diagnostic LIST rather
|
|
39
|
+
# than the exit code which need the predicate.
|
|
40
|
+
def crashed?
|
|
41
|
+
diagnostics.any? { |diagnostic| CrashSignature.discards_file_analysis?(diagnostic) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The diagnostics {#crashed?} answers true for, so a caller can name what it saw rather than only that
|
|
45
|
+
# it saw something.
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<Rigor::Analysis::Diagnostic>]
|
|
48
|
+
def crash_diagnostics
|
|
49
|
+
diagnostics.select { |diagnostic| CrashSignature.discards_file_analysis?(diagnostic) }
|
|
50
|
+
end
|
|
51
|
+
|
|
20
52
|
def error_count
|
|
21
53
|
diagnostics.count(&:error?)
|
|
22
54
|
end
|
|
@@ -14,8 +14,13 @@ end
|
|
|
14
14
|
require_relative "../version"
|
|
15
15
|
require_relative "../cache/descriptor"
|
|
16
16
|
require_relative "../cache/engine_source"
|
|
17
|
+
require_relative "../cache/file_digest"
|
|
17
18
|
require_relative "../cache/rbs_descriptor"
|
|
18
19
|
require_relative "../environment/default_libraries"
|
|
20
|
+
# Both resolvers are leaf files (no requires beyond `yaml`), so the boot-slimming probe reaches the two
|
|
21
|
+
# lockfile paths without loading `rigor/environment` or the RBS machinery.
|
|
22
|
+
require_relative "../environment/lockfile_resolver"
|
|
23
|
+
require_relative "../environment/rbs_collection_discovery"
|
|
19
24
|
|
|
20
25
|
module Rigor
|
|
21
26
|
module Analysis
|
|
@@ -80,7 +85,7 @@ module Rigor
|
|
|
80
85
|
def descriptor(configuration:, files:, explain:, rbs_config_entries:)
|
|
81
86
|
Cache::Descriptor.new(
|
|
82
87
|
gems: [Cache::RbsDescriptor.rbs_gem_entry],
|
|
83
|
-
configs: rbs_config_entries + engine_source_entries + [
|
|
88
|
+
configs: rbs_config_entries + engine_source_entries + lockfile_entries(configuration) + [
|
|
84
89
|
config_entry("configuration", Marshal.dump(configuration.to_h)),
|
|
85
90
|
config_entry("engine",
|
|
86
91
|
"#{Rigor::VERSION}:#{Cache::Descriptor::SCHEMA_VERSION}:#{explain}"),
|
|
@@ -106,6 +111,41 @@ module Rigor
|
|
|
106
111
|
[config_entry("engine-source", identity)]
|
|
107
112
|
end
|
|
108
113
|
|
|
114
|
+
# Issue #564 — the two dependency lockfiles, BY CONTENT. Both are inputs to the run's diagnostics and
|
|
115
|
+
# neither was represented in the key: the locked gem set decides which bundle-shipped `sig/` dirs and
|
|
116
|
+
# which `rbs_collection` dirs load, which ADR-72 gem overlays apply, what the ADR-82 WD9 missing-gem
|
|
117
|
+
# constant index owns, and whether `rbs.coverage.missing-gem` fires at all. Editing a lockfile touches
|
|
118
|
+
# no analyzed source and no `.rbs` file, so the `paths` slot, the `configuration` slot (which carries
|
|
119
|
+
# the lockfile PATH, never its bytes) and the recorded dependency descriptor were all unchanged by a
|
|
120
|
+
# `bundle add` — and a warm run replayed the pre-edit diagnostics in BOTH directions: an
|
|
121
|
+
# `undefined-method` on `3.minutes` surviving the `bundle add activesupport` that licenses the overlay,
|
|
122
|
+
# and the same call staying silent after the `bundle remove` that revokes it.
|
|
123
|
+
#
|
|
124
|
+
# A KEY slot rather than a dependency entry, because a dependency descriptor can only say "a file I
|
|
125
|
+
# recorded changed": it cannot express a lockfile that did not exist when the entry was written and
|
|
126
|
+
# does now, which is the `bundle init` / first-`bundle install` case.
|
|
127
|
+
#
|
|
128
|
+
# Content-only — the resolved PATH is deliberately not in the payload, so a checkout that moves (a CI
|
|
129
|
+
# runner's workspace, a renamed directory) still hits with an identical lockfile.
|
|
130
|
+
def lockfile_entries(configuration)
|
|
131
|
+
bundler = Environment::LockfileResolver.resolve_lockfile_path(
|
|
132
|
+
lockfile_path: configuration.bundler_lockfile, auto_detect: configuration.bundler_auto_detect
|
|
133
|
+
)
|
|
134
|
+
collection = Environment::RbsCollectionDiscovery.resolve_lockfile_path(
|
|
135
|
+
lockfile_path: configuration.rbs_collection_lockfile,
|
|
136
|
+
auto_detect: configuration.rbs_collection_auto_detect
|
|
137
|
+
)
|
|
138
|
+
[lockfile_entry("bundler.lockfile", bundler), lockfile_entry("rbs_collection.lockfile", collection)]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# `nil` — no lockfile resolves — is itself part of the identity, so it carries its own sentinel rather
|
|
142
|
+
# than dropping the slot: "absent" and "present but empty" must not key the same.
|
|
143
|
+
ABSENT_LOCKFILE = "\0absent"
|
|
144
|
+
|
|
145
|
+
def lockfile_entry(key, path)
|
|
146
|
+
config_entry(key, path.nil? ? ABSENT_LOCKFILE : Cache::FileDigest.hexdigest(path.to_s))
|
|
147
|
+
end
|
|
148
|
+
|
|
109
149
|
def config_entry(key, payload)
|
|
110
150
|
Cache::Descriptor::ConfigEntry.new(key: key, value_hash: Digest::SHA256.hexdigest(payload))
|
|
111
151
|
end
|
|
@@ -36,13 +36,16 @@ module Rigor
|
|
|
36
36
|
# because they do not parse (`[path, first_error_line]` pairs).
|
|
37
37
|
# @param env_build_failure_snapshot [#call] reader returning the total RBS env-build failure tuple
|
|
38
38
|
# (`[error_class, first_error_line, conflicting_buffer_names]`) or nil when the env built.
|
|
39
|
+
# @param definition_build_failures_snapshot [#call] issue #696 — reader returning the per-class
|
|
40
|
+
# `RBS::DefinitionBuilder` failures the run observed, as `[class_name, error_class, member,
|
|
41
|
+
# conflicting_buffer_names]` tuples. Empty for a healthy sig set.
|
|
39
42
|
# @param conformance_results_snapshot [#call] reader.
|
|
40
43
|
def initialize(configuration:, rbs_extended_reporter:, boundary_cross_reporter:, # rubocop:disable Metrics/ParameterLists
|
|
41
44
|
source_rbs_synthesis_reporter:, plugin_registry:, dependency_source_index:,
|
|
42
45
|
pool_mode:, cached_plugin_prepare_diagnostics:,
|
|
43
46
|
pre_eval_diagnostics_from_scanner:, synthesized_namespaces_snapshot:,
|
|
44
47
|
quarantined_signatures_snapshot:, env_build_failure_snapshot:,
|
|
45
|
-
conformance_results_snapshot:)
|
|
48
|
+
definition_build_failures_snapshot:, conformance_results_snapshot:)
|
|
46
49
|
@configuration = configuration
|
|
47
50
|
@rbs_extended_reporter = rbs_extended_reporter
|
|
48
51
|
@boundary_cross_reporter = boundary_cross_reporter
|
|
@@ -55,6 +58,7 @@ module Rigor
|
|
|
55
58
|
@synthesized_namespaces_snapshot_reader = synthesized_namespaces_snapshot
|
|
56
59
|
@quarantined_signatures_snapshot_reader = quarantined_signatures_snapshot
|
|
57
60
|
@env_build_failure_snapshot_reader = env_build_failure_snapshot
|
|
61
|
+
@definition_build_failures_snapshot_reader = definition_build_failures_snapshot
|
|
58
62
|
@conformance_results_snapshot_reader = conformance_results_snapshot
|
|
59
63
|
end
|
|
60
64
|
|
|
@@ -351,6 +355,32 @@ module Rigor
|
|
|
351
355
|
[build_rbs_environment_build_failed_diagnostic(failure)]
|
|
352
356
|
end
|
|
353
357
|
|
|
358
|
+
# Issue #696 — the third rung of the same ladder, between its two neighbours by consequence: a
|
|
359
|
+
# QUARANTINED file removes what one FILE declared, this removes what one CLASS declared, an
|
|
360
|
+
# ENV-BUILD failure removes everything. `RBS::DefinitionBuilder` raising for a class (typically
|
|
361
|
+
# `DuplicatedMethodDefinitionError`: two signature sources declaring the same method, e.g. a project
|
|
362
|
+
# `sig/` carrying a vendored COPY of a bundled plugin's `.rbs`) leaves the class KNOWN but with no
|
|
363
|
+
# method surface, so every call on it — real methods and typos alike — reads `Dynamic[top]`. When
|
|
364
|
+
# the collision is on a class others inherit, the whole bundled type universe goes with it: the
|
|
365
|
+
# observed shape is thousands of stderr warnings, ZERO diagnostics, and exit 0.
|
|
366
|
+
#
|
|
367
|
+
# Not "drop the class to genuinely-unknown" instead. That also removes diagnostics, so it is not
|
|
368
|
+
# louder; it changes `Dynamic[top]` semantics for every rule that consults `class_known?`; and
|
|
369
|
+
# `class_known?` reads {Environment::RbsLoader#known_class_names_set}, a different table from the
|
|
370
|
+
# definition builder, so it would need a new "known but unbuildable" state regardless. The failure
|
|
371
|
+
# is made REPORTABLE; the class is not made invisible.
|
|
372
|
+
#
|
|
373
|
+
# Authored `:warning`, not `:error`, for the same reason as both neighbours: the collision is
|
|
374
|
+
# typically between the user's `sig/` and Rigor's OWN bundled RBS, so an `:error` default would let
|
|
375
|
+
# a Rigor release turn a green build red with no user change (ADR-5 / AGENTS.md § FP discipline).
|
|
376
|
+
# The `reject-unparseable-signatures` bleeding-edge feature promotes it to `:error`.
|
|
377
|
+
def rbs_definition_build_failed_diagnostics
|
|
378
|
+
failures = definition_build_failures_snapshot
|
|
379
|
+
return [] if failures.nil? || failures.empty?
|
|
380
|
+
|
|
381
|
+
[build_rbs_definition_build_failed_diagnostic(failures)]
|
|
382
|
+
end
|
|
383
|
+
|
|
354
384
|
def rbs_synthesized_namespace_diagnostics
|
|
355
385
|
synthesized = synthesized_namespaces_snapshot
|
|
356
386
|
return [] if synthesized.nil? || synthesized.empty?
|
|
@@ -462,6 +492,64 @@ module Rigor
|
|
|
462
492
|
)
|
|
463
493
|
end
|
|
464
494
|
|
|
495
|
+
# Issue #696. One diagnostic per RUN, not per class: a collision on a widely-inherited class fails
|
|
496
|
+
# every descendant, and 2,709 rows saying the same thing about one `.rbs` line is noise, not
|
|
497
|
+
# visibility.
|
|
498
|
+
#
|
|
499
|
+
# The failed-class list alone is not actionable, and that is what the "First failure" clause is for.
|
|
500
|
+
# A duplicate on `::Object#blank?` fails `String`, `Integer`, `Array` and 1,333 more — every name in
|
|
501
|
+
# that list is a DESCENDANT, and none of them is where the fix goes. The member comes off the error
|
|
502
|
+
# object rather than out of its message ({RbsLoader#definition_build_member}), so it names the
|
|
503
|
+
# culprit and reads the same on a cache hit, where every `RBS::Location` has been dumped to a
|
|
504
|
+
# sentinel. The error class is attributed to that first failure explicitly rather than to all of
|
|
505
|
+
# them: the per-class memo keeps only the first reason, and one run can mix error classes.
|
|
506
|
+
def build_rbs_definition_build_failed_diagnostic(failures)
|
|
507
|
+
sample_size = 5
|
|
508
|
+
files = failures.flat_map { |failure| Array(failure[3]) }.uniq.map { |name| relative_signature_path(name) }
|
|
509
|
+
Diagnostic.new(
|
|
510
|
+
path: ".rigor.yml",
|
|
511
|
+
line: 1,
|
|
512
|
+
column: 1,
|
|
513
|
+
message: "#{failures.size} RBS class definition(s) failed to build: " \
|
|
514
|
+
"#{sampled(failures.map(&:first), sample_size)}." \
|
|
515
|
+
"#{first_failure_clause(failures.first)}#{conflicting_files_clause(files, sample_size)} " \
|
|
516
|
+
"Rigor still treats each class as KNOWN, so calls into it — real methods and typos " \
|
|
517
|
+
"alike — silently read `Dynamic[top]` instead of resolving, and this run is quieter " \
|
|
518
|
+
"than it should be rather than cleaner. Two signature sources declare the same " \
|
|
519
|
+
"member; remove the duplicate declaration (`rbs validate`) to restore type coverage.",
|
|
520
|
+
severity: :warning,
|
|
521
|
+
rule: "rbs.coverage.definition-build-failed",
|
|
522
|
+
source_family: :builtin
|
|
523
|
+
)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# `[class_name, error_class, member, buffers]`. The member is nil for the error classes that carry no
|
|
527
|
+
# name at all (`RecursiveAncestorError`), and the clause then names the error class alone rather than
|
|
528
|
+
# inventing a member.
|
|
529
|
+
def first_failure_clause(failure)
|
|
530
|
+
_, error_class, member, = failure
|
|
531
|
+
return " First failure: #{error_class}." if member.nil? || member.empty?
|
|
532
|
+
|
|
533
|
+
" First failure: #{error_class} on `#{member}`."
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Identical on a cache HIT: the ADR-54 env cache drops location POSITIONS but keeps each buffer's
|
|
537
|
+
# NAME, so a warm run names the same files a cold one does. Where a name is absent anyway, the
|
|
538
|
+
# reconstructed {RbsLoader::CACHED_LOCATION_BUFFER_NAME} sentinel is filtered out and the clause is
|
|
539
|
+
# omitted, rather than naming `<cached>` as if it were a path.
|
|
540
|
+
def conflicting_files_clause(files, sample_size)
|
|
541
|
+
return "" if files.empty?
|
|
542
|
+
|
|
543
|
+
" Conflicting signature file(s): #{sampled(files, sample_size)}."
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# "a, b, c, and N more" — the sampling shape every `rbs.coverage.*` message above uses.
|
|
547
|
+
def sampled(names, sample_size)
|
|
548
|
+
sample = names.first(sample_size)
|
|
549
|
+
suffix = names.size > sample_size ? ", and #{names.size - sample_size} more" : ""
|
|
550
|
+
"#{sample.join(', ')}#{suffix}"
|
|
551
|
+
end
|
|
552
|
+
|
|
465
553
|
# The absolute path is what the loader records; the user thinks in project-relative terms.
|
|
466
554
|
def relative_signature_path(path)
|
|
467
555
|
root = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
@@ -684,6 +772,10 @@ module Rigor
|
|
|
684
772
|
@env_build_failure_snapshot_reader.call
|
|
685
773
|
end
|
|
686
774
|
|
|
775
|
+
def definition_build_failures_snapshot
|
|
776
|
+
@definition_build_failures_snapshot_reader.call
|
|
777
|
+
end
|
|
778
|
+
|
|
687
779
|
def conformance_results_snapshot
|
|
688
780
|
@conformance_results_snapshot_reader.call
|
|
689
781
|
end
|