rigortype 0.4.4 → 0.4.6
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/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
# Issue #2021 — the files a run analyses outside the configured `paths:` load through `require_relative`.
|
|
8
|
+
#
|
|
9
|
+
# A run over path arguments widens its discovery to the configured paths (#684), which is the whole project for
|
|
10
|
+
# a file inside them. A file outside them has no such project: `rigor check plugins/x/lib/rigor/plugin/x.rb`
|
|
11
|
+
# in a repository whose `paths:` is `lib` discovered `x.rb` and `lib` alone. The plugin's nested `Effects`,
|
|
12
|
+
# declared in the sibling `x/effects.rb`, was then invisible, the lexical lookup fell through to the
|
|
13
|
+
# project's `Rigor::Effects`, and every call on it drew a false `call.undefined-method` that checking the
|
|
14
|
+
# plugin's `lib` did not.
|
|
15
|
+
#
|
|
16
|
+
# The file names its siblings itself: Ruby loads what it `require_relative`s before any of its code runs, so
|
|
17
|
+
# those declarations are in scope at runtime. {.closure} follows that graph from the out-of-paths files,
|
|
18
|
+
# through each target that is itself outside the configured paths. A target under a configured path is
|
|
19
|
+
# already discovered and stops the walk there, so the files inside `paths:` are discovered exactly as a run
|
|
20
|
+
# over the configured paths discovers them. Only a string literal counts (`require_relative "a/b"`, with or
|
|
21
|
+
# without the `.rb`); a computed name, `require`, `load` and `autoload` are not followed, so a gem's own
|
|
22
|
+
# files and a Zeitwerk-loaded tree are not reached this way.
|
|
23
|
+
#
|
|
24
|
+
# The walk sees one entry point's load graph, and a file it does not reach may redefine what a walked file
|
|
25
|
+
# defines. The runner therefore discovers the walked files as uncertain: their declarations resolve constants
|
|
26
|
+
# and their methods exist, but no return type, arity or visibility is read from them.
|
|
27
|
+
module RequireRelativeClosure
|
|
28
|
+
# The walk's answer: the existing targets it reached, and the targets it named that are not files (each an
|
|
29
|
+
# input of the answer too, since one appearing would join the discovery).
|
|
30
|
+
Result = Data.define(:found, :missing)
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# @param seeds — the analysed files outside the configured paths, as spelled by the run.
|
|
35
|
+
# @param covered — `->(real_path) { Boolean }`, whether a target lies under a configured path, where discovery
|
|
36
|
+
# holds it already. The walk neither adds nor follows such a target. Paths are compared as real paths, so a
|
|
37
|
+
# symlinked spelling of a configured file is covered too.
|
|
38
|
+
# @param read — `->(path) { String }`, the source of a file (an editor buffer's bytes for its file).
|
|
39
|
+
# @return a {Result}; `found` holds absolute paths in walk order, each file once, and never a seed.
|
|
40
|
+
def closure(seeds, covered:, read:)
|
|
41
|
+
seen = seeds.to_set { |path| real_path(path) }
|
|
42
|
+
queue = seeds.dup
|
|
43
|
+
found = []
|
|
44
|
+
missing = []
|
|
45
|
+
until queue.empty?
|
|
46
|
+
targets_of(queue.shift, read).each do |target|
|
|
47
|
+
next missing << target unless File.file?(target)
|
|
48
|
+
|
|
49
|
+
real = real_path(target)
|
|
50
|
+
next if covered.call(real) || !seen.add?(real)
|
|
51
|
+
|
|
52
|
+
found << target
|
|
53
|
+
queue << target
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
Result.new(found: found, missing: missing.uniq)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def real_path(path)
|
|
60
|
+
File.realpath(path)
|
|
61
|
+
rescue SystemCallError
|
|
62
|
+
File.absolute_path(path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The `.rb` paths `path` names with a literal `require_relative`, absolute, whether or not they exist.
|
|
66
|
+
def targets_of(path, read)
|
|
67
|
+
source = read.call(path)
|
|
68
|
+
return [] if source.nil?
|
|
69
|
+
|
|
70
|
+
base = File.dirname(File.absolute_path(path))
|
|
71
|
+
literal_features(Prism.parse(source).value).map do |feature|
|
|
72
|
+
File.expand_path(feature.end_with?(".rb") ? feature : "#{feature}.rb", base)
|
|
73
|
+
end
|
|
74
|
+
rescue StandardError
|
|
75
|
+
[]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# In source order, so discovery meets the targets in the order Ruby loads them.
|
|
79
|
+
def literal_features(root)
|
|
80
|
+
calls = []
|
|
81
|
+
stack = [root]
|
|
82
|
+
until stack.empty?
|
|
83
|
+
node = stack.pop
|
|
84
|
+
calls << node if literal_feature(node)
|
|
85
|
+
stack.concat(node.compact_child_nodes)
|
|
86
|
+
end
|
|
87
|
+
calls.sort_by { |call| call.location.start_offset }.filter_map { |call| literal_feature(call) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def literal_feature(node)
|
|
91
|
+
return nil unless node.is_a?(Prism::CallNode) && node.name == :require_relative && node.receiver.nil?
|
|
92
|
+
|
|
93
|
+
arguments = node.arguments&.arguments
|
|
94
|
+
return nil unless arguments&.size == 1 && arguments.first.is_a?(Prism::StringNode)
|
|
95
|
+
|
|
96
|
+
arguments.first.unescaped
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -99,7 +99,12 @@ module Rigor
|
|
|
99
99
|
"Rigor's bundled overlays name (`OpenStruct`, whose fields are per-instance singleton methods; " \
|
|
100
100
|
"`ActiveSupport::Duration` and `ActiveSupport::Concern` when their gem overlay loads). Both sides " \
|
|
101
101
|
"of the class are open, and an inherited Object / Kernel signature no longer decides " \
|
|
102
|
-
"`call.wrong-arity` or `call.argument-type-mismatch` on it."
|
|
102
|
+
"`call.wrong-arity` or `call.argument-type-mismatch` on it.",
|
|
103
|
+
"The program may answer the call another way (#1722): a `method_missing` on the receiver's class or an " \
|
|
104
|
+
"ancestor; a module mixed into either that defines the method (`Integer.include(Ext)` counts wherever " \
|
|
105
|
+
"it is written); singleton methods given to the receiver local (`s.extend(M)`, " \
|
|
106
|
+
"`define_singleton_method`, `class << s`); or, for a receiver typed by its class's name rather than a " \
|
|
107
|
+
"literal, a project subclass that defines the method."
|
|
103
108
|
],
|
|
104
109
|
suppression: "`# rigor:disable call.undefined-method` on the call line, " \
|
|
105
110
|
"or `disable: [\"call.undefined-method\"]` in `.rigor.yml`.",
|
|
@@ -561,14 +566,22 @@ module Rigor
|
|
|
561
566
|
summary: "Explicit-receiver call to a method declared `private` in source.",
|
|
562
567
|
fires_when: [
|
|
563
568
|
"Call is `receiver.method(...)` with explicit non-self receiver.",
|
|
564
|
-
"Receiver type resolves to `Type::Nominal[X]`.",
|
|
565
|
-
"X is a user-defined class whose source carries the method under `private
|
|
569
|
+
"Receiver type resolves to `Type::Nominal[X]`, or to the class object `singleton(X)`.",
|
|
570
|
+
"X is a user-defined class whose source carries the method under `private`; for a class object, " \
|
|
571
|
+
"the singleton method the call reaches is private (`private_class_method`, `class << self` with " \
|
|
572
|
+
"`private`, or a private method of a module X extends)."
|
|
566
573
|
],
|
|
567
574
|
does_not_fire_when: [
|
|
568
575
|
"Implicit-self call (no receiver) — always allowed for private.",
|
|
569
576
|
"Receiver is `self` (Ruby 2.7+ permits `self.private_method`).",
|
|
570
577
|
"Receiver class is RBS-known but not user-source-defined (RBS-side visibility is deferred).",
|
|
571
|
-
"Method is `:protected` (subclass tracking is deferred)."
|
|
578
|
+
"Method is `:protected` (subclass tracking is deferred).",
|
|
579
|
+
"A `method_missing` or `respond_to_missing?` on the receiver's chain, on the side the call reaches, " \
|
|
580
|
+
"may answer the call (Ruby sends a private call to `method_missing`).",
|
|
581
|
+
"The visibility rests on a statement that may not run, or on files that disagree about it.",
|
|
582
|
+
"On a class object: the receiver is not a constant (`self.class.x` may be a subclass that " \
|
|
583
|
+
"re-publicises the name), or the call is not inside a `def` (load-time code may run before the " \
|
|
584
|
+
"method is made private)."
|
|
572
585
|
],
|
|
573
586
|
suppression: "`# rigor:disable method-visibility-mismatch`.",
|
|
574
587
|
severity_authored: :error,
|
|
@@ -693,7 +706,11 @@ module Rigor
|
|
|
693
706
|
"Writes are in a singleton body — `def self.foo` or a `class << self` def — those track " \
|
|
694
707
|
"separately.",
|
|
695
708
|
"Writes are in a `def` inside an anonymous-class factory block (`Class.new do … end`, " \
|
|
696
|
-
"`Module.new`, `Struct.new`, `Data.define`): that body defines another class."
|
|
709
|
+
"`Module.new`, `Struct.new`, `Data.define`): that body defines another class.",
|
|
710
|
+
"The class descends from a plugin-declared per-invocation class (ADR-125; rigor-actionpack declares " \
|
|
711
|
+
"`ActionController::Base` / `ActionController::API`, rigor-actionmailer `ActionMailer::Base`), and the " \
|
|
712
|
+
"write and every conflicting write sit in different actions: public methods no callback macro or " \
|
|
713
|
+
"`helper_method` names. One action runs per instance."
|
|
697
714
|
],
|
|
698
715
|
suppression: "`# rigor:disable ivar-write-mismatch` on the offending write.",
|
|
699
716
|
severity_authored: :error,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# Issue #2020 — what one file's analysis contributed to the run-scoped plugin tables ({Plugin::Base#disclose_once}
|
|
6
|
+
# and {Plugin::Base#emit_once}), kept beside the file's cached answer so an `--incremental` recheck that serves
|
|
7
|
+
# the file without analysing it can still report the rows a cold run reports.
|
|
8
|
+
#
|
|
9
|
+
# A replayed row must go stale with the inputs it is about, and the reads those inputs came from are credited to
|
|
10
|
+
# one file at most: a plugin memoises the input (a locale index, a routes table), so the first file to trigger
|
|
11
|
+
# the read is the only one credited with it, and a read in `#prepare` is credited to none. So each record also
|
|
12
|
+
# carries, under `:reads`, every row the registering plugin's boundary holds once the file is analysed,
|
|
13
|
+
# `#prepare`'s included. {IncrementalSession} keeps those rows per `[plugin id, key]`, not per file: when one goes
|
|
14
|
+
# stale it re-analyses one registering file and drops the key's replays, rather than re-analysing every file that
|
|
15
|
+
# registered it. Only inputs read through the boundary are covered.
|
|
16
|
+
module RunDisclosureCredit
|
|
17
|
+
ATTRIBUTION_KEY = :rigor_run_disclosure_attribution
|
|
18
|
+
private_constant :ATTRIBUTION_KEY
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Runs the block with run-scoped registrations attributed and returns the file's records — each a run-disclosure
|
|
23
|
+
# record tagged with its `plugin_id` and carrying the registering plugin's boundary rows as `:reads`, one per
|
|
24
|
+
# `[plugin id, key]` — frozen and Marshal-clean. A thread variable, as {Plugin::IoBoundary.attributing} uses, so a
|
|
25
|
+
# registration from inside an `Enumerator` still lands.
|
|
26
|
+
def collect
|
|
27
|
+
registrations = []
|
|
28
|
+
previous = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
|
|
29
|
+
Thread.current.thread_variable_set(ATTRIBUTION_KEY, registrations)
|
|
30
|
+
begin
|
|
31
|
+
yield
|
|
32
|
+
ensure
|
|
33
|
+
Thread.current.thread_variable_set(ATTRIBUTION_KEY, previous)
|
|
34
|
+
end
|
|
35
|
+
credit(registrations)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Called by {Plugin::Base#disclose_once} and {Plugin::Base#emit_once} for every call, a repeat included.
|
|
39
|
+
def attribute(plugin, record)
|
|
40
|
+
Thread.current.thread_variable_get(ATTRIBUTION_KEY)&.push([plugin, record])
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def credit(registrations)
|
|
45
|
+
return [].freeze if registrations.empty?
|
|
46
|
+
|
|
47
|
+
reads = {}.compare_by_identity
|
|
48
|
+
records = registrations.map do |plugin, record|
|
|
49
|
+
rows = reads[plugin] ||= boundary_rows(plugin).freeze
|
|
50
|
+
record.merge(plugin_id: plugin_id(plugin), reads: rows).freeze
|
|
51
|
+
end
|
|
52
|
+
records.uniq { |record| identity(record) }.freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The de-duplication identity a record shares with every other registration of its key.
|
|
56
|
+
def identity(record)
|
|
57
|
+
[record[:plugin_id], record[:key]]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The record as the aggregator replays it, without the rows that decide whether it is stale.
|
|
61
|
+
def replayable(record)
|
|
62
|
+
record.except(:reads).freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The boundary's file and listing rows, read without triggering its lazy initializer: a plugin that never
|
|
66
|
+
# built one read nothing through it.
|
|
67
|
+
def boundary_rows(plugin)
|
|
68
|
+
boundary = plugin.instance_variable_get(:@io_boundary) or return []
|
|
69
|
+
descriptor = boundary.cache_descriptor
|
|
70
|
+
descriptor.files + descriptor.globs
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def plugin_id(plugin)
|
|
74
|
+
plugin.manifest.id
|
|
75
|
+
rescue StandardError
|
|
76
|
+
plugin.class.to_s
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run_disclosure_credit"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
# Issue #2020 — the `--incremental` session's record of the run-scoped plugin rows ({Plugin::Base#disclose_once},
|
|
8
|
+
# {Plugin::Base#emit_once}) each analysed file's hooks registered, so a recheck can replay them for the files it
|
|
9
|
+
# serves from cache.
|
|
10
|
+
#
|
|
11
|
+
# - `files` maps a path to its records as the aggregator replays them, one per `[plugin id, key]` identity.
|
|
12
|
+
# - `reads` maps an identity to the registering plugin's boundary rows when it was last registered
|
|
13
|
+
# ({RunDisclosureCredit}), `#prepare`'s reads included.
|
|
14
|
+
#
|
|
15
|
+
# Staleness is per identity, not per file. A plugin memoises the input a notice is about and re-registers the
|
|
16
|
+
# notice from every file's hook, so crediting the rows to every registering file would re-analyse all of them on
|
|
17
|
+
# any edit to an input the plugin read (one view edit re-analysed 400 of 400 files in an app with one i18n view
|
|
18
|
+
# row). Instead a recheck finds the identities whose rows went stale, re-analyses ONE live registering file for
|
|
19
|
+
# each (the first by path), and withholds those identities' replays. When that file registers the identity, every
|
|
20
|
+
# other holder takes its record. When it registers nothing — the edit took the notice out of its text, it no
|
|
21
|
+
# longer parses, the plugin raised — the recheck cannot tell whether the identity is gone, so it runs again with
|
|
22
|
+
# every other live holder of the identity analysed ({#unresolved_holders}), and the identity is dropped only if
|
|
23
|
+
# none of them registers it either.
|
|
24
|
+
#
|
|
25
|
+
# One shape stays unsound: a key whose registration or record depends both on an input the plugin read and on the
|
|
26
|
+
# registering file's own text. When the input changes and the chosen file still registers the key, the other
|
|
27
|
+
# holders take its record even where, re-analysed, they would register another record or none, until each is
|
|
28
|
+
# re-analysed itself. {Plugin::Base#disclose_once} asks a key not to vary between workers, which rules it out for a
|
|
29
|
+
# plugin that keeps the contract. Only inputs read through the plugin's boundary are tracked; state a plugin carries
|
|
30
|
+
# across files, or an input it reads around the boundary (`ENV`, a direct `File.read`), can leave a replayed row
|
|
31
|
+
# that never clears.
|
|
32
|
+
class RunDisclosureLedger
|
|
33
|
+
attr_reader :files, :reads
|
|
34
|
+
|
|
35
|
+
def initialize(files: {}, reads: {})
|
|
36
|
+
@files = files
|
|
37
|
+
@reads = reads
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The ledger a baseline builds from `fresh` (`{path => [record with :reads]}`, `Runner#analysis_disclosures`).
|
|
41
|
+
def self.from(fresh)
|
|
42
|
+
new.tap { |ledger| ledger.absorb(fresh, fresh.keys, Set.new) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.load(data)
|
|
46
|
+
data.is_a?(Hash) ? new(files: data.fetch(:files, {}), reads: data.fetch(:reads, {})) : new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_h
|
|
50
|
+
{ files: @files, reads: @reads }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The identities with a boundary row that no longer validates under `fresh_row` (a row → Boolean predicate).
|
|
54
|
+
def stale_identities(&fresh_row)
|
|
55
|
+
verdicts = {}
|
|
56
|
+
@reads.each_with_object(Set.new) do |(identity, rows), stale|
|
|
57
|
+
stale << identity if rows.any? { |row| !verdicts.fetch(row) { verdicts[row] = fresh_row.call(row) } }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# One live registering file per stale identity, the first by path, skipping `removed`.
|
|
62
|
+
def refreshers(stale, removed)
|
|
63
|
+
return Set.new if stale.empty?
|
|
64
|
+
|
|
65
|
+
holders = {}
|
|
66
|
+
@files.each do |path, records|
|
|
67
|
+
next if removed.include?(path)
|
|
68
|
+
|
|
69
|
+
records.each do |record|
|
|
70
|
+
identity = RunDisclosureCredit.identity(record)
|
|
71
|
+
holders[identity] = [holders[identity], path].compact.min if stale.include?(identity)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
holders.values.to_set
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The live holders, outside `analyzed`, of every `stale` identity no file in `analyzed` registered in `fresh`:
|
|
78
|
+
# the files that must be analysed when the chosen refresher registered nothing. Analysing every one of them
|
|
79
|
+
# settles the identity, since every holder has then decided it afresh.
|
|
80
|
+
def unresolved_holders(fresh, analyzed, stale, removed)
|
|
81
|
+
return Set.new if stale.empty?
|
|
82
|
+
|
|
83
|
+
registered = analyzed.flat_map do |path|
|
|
84
|
+
Array(fresh[path]).map { |record| RunDisclosureCredit.identity(record) }
|
|
85
|
+
end
|
|
86
|
+
unresolved = stale - registered
|
|
87
|
+
return Set.new if unresolved.empty?
|
|
88
|
+
|
|
89
|
+
@files.each_with_object(Set.new) do |(path, records), holders|
|
|
90
|
+
next if removed.include?(path) || analyzed.include?(path)
|
|
91
|
+
|
|
92
|
+
holders << path if records.any? { |record| unresolved.include?(RunDisclosureCredit.identity(record)) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The records to replay for `paths`, without the `stale` identities. Unpinned defence: a stale identity's replay
|
|
97
|
+
# cannot reach the output today, since a refresher that registers it wins over the replay and one that does not
|
|
98
|
+
# sends every holder into the second pass.
|
|
99
|
+
def served(paths, stale = Set.new)
|
|
100
|
+
@files.values_at(*paths).compact.flatten(1).reject do |record|
|
|
101
|
+
stale.include?(RunDisclosureCredit.identity(record))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Folds a run's registrations in: every path in `analyzed` takes this run's records (or none), every identity
|
|
106
|
+
# this run registered takes this run's rows, and each `stale` identity is refreshed or dropped in the files the
|
|
107
|
+
# run did not analyse. Identities no file holds any more lose their rows.
|
|
108
|
+
def absorb(fresh, analyzed, stale)
|
|
109
|
+
registered = {}
|
|
110
|
+
analyzed.each do |path|
|
|
111
|
+
records = Array(fresh[path])
|
|
112
|
+
records.each { |record| registered[RunDisclosureCredit.identity(record)] ||= record }
|
|
113
|
+
store(path, records.map { |record| RunDisclosureCredit.replayable(record) })
|
|
114
|
+
end
|
|
115
|
+
registered.each { |identity, record| @reads[identity] = record[:reads] }
|
|
116
|
+
refresh_unanalysed(stale, registered, analyzed.to_set)
|
|
117
|
+
prune
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def forget(path)
|
|
121
|
+
@files.delete(path)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def refresh_unanalysed(stale, registered, analyzed)
|
|
127
|
+
return if stale.empty?
|
|
128
|
+
|
|
129
|
+
@files.each_key.to_a.each do |path|
|
|
130
|
+
next if analyzed.include?(path)
|
|
131
|
+
|
|
132
|
+
store(path, @files[path].filter_map do |record|
|
|
133
|
+
identity = RunDisclosureCredit.identity(record)
|
|
134
|
+
next record unless stale.include?(identity)
|
|
135
|
+
|
|
136
|
+
registered[identity] && RunDisclosureCredit.replayable(registered[identity])
|
|
137
|
+
end)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def store(path, records)
|
|
142
|
+
records.empty? ? @files.delete(path) : @files[path] = records.freeze
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Unpinned hygiene: an identity no file holds is never replayed, so keeping its rows changes no answer; pruning
|
|
146
|
+
# them keeps a stale row from re-analysing a file for nothing and the snapshot from growing.
|
|
147
|
+
def prune
|
|
148
|
+
held = @files.each_value.flat_map do |records|
|
|
149
|
+
records.map { |record| RunDisclosureCredit.identity(record) }
|
|
150
|
+
end.to_set
|
|
151
|
+
@reads.select! { |identity, _| held.include?(identity) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -79,6 +79,8 @@ module Rigor
|
|
|
79
79
|
# (`ProjectContext#environment`) every job's Runner reuses instead of rebuilding.
|
|
80
80
|
# @param prebuilt — the warm, shared pre-pass snapshot
|
|
81
81
|
# (`ProjectContext#project_scan`) every job's Runner adopts instead of re-scanning.
|
|
82
|
+
# @param discovery_seed_for — issue #2147 — a callable returning the cross-file discovery seed for one
|
|
83
|
+
# binding (`ProjectContext#discovery_seed_for`), or nil for none.
|
|
82
84
|
# @param workers — pool size. `#analyze` degrades to sequential in-process execution — one
|
|
83
85
|
# job at a time, no `fork` — when fewer than 2 bindings are submitted, fewer than `min_batch_size`
|
|
84
86
|
# bindings are submitted, `workers` is not positive, `fork` is unavailable on this platform, or
|
|
@@ -88,12 +90,13 @@ module Rigor
|
|
|
88
90
|
# @param min_batch_size — see {DEFAULT_MIN_BATCH_SIZE}. Exposed as a constructor param
|
|
89
91
|
# (rather than read from the env internally) purely for spec control; production callers get the
|
|
90
92
|
# resolved default.
|
|
91
|
-
def initialize(configuration:, cache_store:, environment:, prebuilt:, workers:,
|
|
93
|
+
def initialize(configuration:, cache_store:, environment:, prebuilt:, workers:, discovery_seed_for: nil,
|
|
92
94
|
min_batch_size: self.class.resolve_min_batch_size)
|
|
93
95
|
@configuration = configuration
|
|
94
96
|
@cache_store = cache_store
|
|
95
97
|
@environment = environment
|
|
96
98
|
@prebuilt = prebuilt
|
|
99
|
+
@discovery_seed_for = discovery_seed_for
|
|
97
100
|
@workers = workers
|
|
98
101
|
@min_batch_size = min_batch_size
|
|
99
102
|
end
|
|
@@ -123,7 +126,8 @@ module Rigor
|
|
|
123
126
|
def run_one(binding)
|
|
124
127
|
Runner.new(
|
|
125
128
|
configuration: @configuration, cache_store: @cache_store, collect_stats: false,
|
|
126
|
-
buffer: binding, prebuilt: @prebuilt, environment: @environment
|
|
129
|
+
buffer: binding, prebuilt: @prebuilt, environment: @environment,
|
|
130
|
+
discovery_seed: @discovery_seed_for&.call(binding)
|
|
127
131
|
).run([binding.logical_path]).diagnostics
|
|
128
132
|
end
|
|
129
133
|
|
|
@@ -58,7 +58,7 @@ module Rigor
|
|
|
58
58
|
definition_build_failures_snapshot:, hkt_scan_failure_snapshot:,
|
|
59
59
|
conformance_results_snapshot:, signature_standdowns_snapshot: -> { [] },
|
|
60
60
|
vendored_standdowns_snapshot: -> { [] }, pooled_run_disclosures: -> { [] },
|
|
61
|
-
pooled_trust_refusals: -> { [] })
|
|
61
|
+
served_run_disclosures: -> { [] }, pooled_trust_refusals: -> { [] })
|
|
62
62
|
@configuration = configuration
|
|
63
63
|
@rbs_extended_reporter = rbs_extended_reporter
|
|
64
64
|
@boundary_cross_reporter = boundary_cross_reporter
|
|
@@ -79,6 +79,8 @@ module Rigor
|
|
|
79
79
|
# Issue #1051 — reader for the pool coordinator's de-duplicated run-scoped disclosure table.
|
|
80
80
|
# Empty on a sequential run, where `plugin_registry`'s own instances are the ones that registered.
|
|
81
81
|
@pooled_run_disclosures_reader = pooled_run_disclosures
|
|
82
|
+
# Issue #2020 — reader for the rows an incremental recheck replays for the files it served unanalysed.
|
|
83
|
+
@served_run_disclosures_reader = served_run_disclosures
|
|
82
84
|
# Issue #1900 — reader for the pool coordinator's summed trust-refusal table. Empty on a
|
|
83
85
|
# sequential run, where `plugin_registry`'s own boundaries made every read.
|
|
84
86
|
@pooled_trust_refusals_reader = pooled_trust_refusals
|
|
@@ -1116,12 +1118,14 @@ module Rigor
|
|
|
1116
1118
|
end
|
|
1117
1119
|
|
|
1118
1120
|
# Issue #1051 — the run-scoped plugin disclosures ({Plugin::Base#disclose_once}), the run's
|
|
1119
|
-
# project-global notices about its own inputs.
|
|
1121
|
+
# project-global notices about its own inputs. Three sources feed one table:
|
|
1120
1122
|
#
|
|
1121
1123
|
# - the coordinator-side registry, which is where a SEQUENTIAL run's `#prepare` and per-file hooks
|
|
1122
1124
|
# ran; and
|
|
1123
1125
|
# - `pooled_run_disclosures`, the pool coordinator's already-de-duplicated collection from the
|
|
1124
|
-
# pre-fork parent session and every worker payload
|
|
1126
|
+
# pre-fork parent session and every worker payload; and
|
|
1127
|
+
# - `served_run_disclosures` (#2020), the rows an incremental recheck replays for the files it served
|
|
1128
|
+
# from cache, since their per-file hooks did not run.
|
|
1125
1129
|
#
|
|
1126
1130
|
# Both are keyed `[plugin id, key]` and the first registration of a pair wins, so a run emits one
|
|
1127
1131
|
# row per pair no matter how many plugin instances (workers) reached it — the #1051 bug, where a
|
|
@@ -1183,6 +1187,10 @@ module Rigor
|
|
|
1183
1187
|
rescue StandardError
|
|
1184
1188
|
next
|
|
1185
1189
|
end
|
|
1190
|
+
# Issue #2020 — last, so a row this run registered afresh wins over the replay of the same key.
|
|
1191
|
+
Array(@served_run_disclosures_reader.call).each do |record|
|
|
1192
|
+
records[[record[:plugin_id], record[:key]]] ||= record
|
|
1193
|
+
end
|
|
1186
1194
|
records.values
|
|
1187
1195
|
end
|
|
1188
1196
|
|
|
@@ -111,6 +111,19 @@ module Rigor
|
|
|
111
111
|
# so the incremental session's dependency graph is refreshed whether analysis ran sequential or pooled.
|
|
112
112
|
attr_reader :collected_dependencies
|
|
113
113
|
|
|
114
|
+
# Issue #1553 — the per-file plugin reads the fork pool's workers recorded this run (empty unless
|
|
115
|
+
# `record_dependencies:` and a pool run occurred), folded into `Runner#analysis_reads`. Each worker's are
|
|
116
|
+
# marshalled back beside its dependency records.
|
|
117
|
+
def collected_analysis_reads
|
|
118
|
+
@collected_analysis_reads ||= {}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Issue #2020 — the per-file run-scoped registrations the fork pool's workers recorded, folded into
|
|
122
|
+
# `Runner#analysis_disclosures` beside {#collected_analysis_reads}.
|
|
123
|
+
def collected_analysis_disclosures
|
|
124
|
+
@collected_analysis_disclosures ||= {}
|
|
125
|
+
end
|
|
126
|
+
|
|
114
127
|
# ADR-103 WD12 — the per-file {Effects::FileCollection}s the fork pool captured this run (empty
|
|
115
128
|
# unless the configuration carries an `effects:` block and a pool run occurred). The runner merges
|
|
116
129
|
# these with its own sequential records before the post-pool fixpoint.
|
|
@@ -197,13 +210,17 @@ module Rigor
|
|
|
197
210
|
if pool_mode?
|
|
198
211
|
dispatch_pool(files, source_files: source_files)
|
|
199
212
|
else
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
213
|
+
environment ||= resolve_sequential_environment(source_files: source_files)
|
|
214
|
+
analyze_files_sequentially(files, environment)
|
|
203
215
|
end
|
|
204
216
|
# Issues #796 / #794 — a narrowed run's own demand set is a SUBSET of the full run's, so the two
|
|
205
|
-
# rows below are folded in after it, whichever backend produced the per-file stream.
|
|
206
|
-
|
|
217
|
+
# rows below are folded in after it, whichever backend produced the per-file stream. Issue #1929 —
|
|
218
|
+
# the sequential run's environment re-derives a stale replayed failure; a pool run has none on
|
|
219
|
+
# this side and resolves one only when a stale failure needs it.
|
|
220
|
+
redemand_env = lambda do
|
|
221
|
+
environment ||= resolve_sequential_environment(source_files: source_files)
|
|
222
|
+
end
|
|
223
|
+
replayed_restored_run_level_rows?(closure: files, redemand_env: redemand_env)
|
|
207
224
|
result
|
|
208
225
|
end
|
|
209
226
|
|
|
@@ -261,7 +278,9 @@ module Rigor
|
|
|
261
278
|
# `env` rather than `resolve.call`: read the demand the snapshots above just made when they built
|
|
262
279
|
# an environment, and force nothing when they did not.
|
|
263
280
|
record_definition_build_failures(env&.rbs_loader&.definition_build_failures)
|
|
264
|
-
|
|
281
|
+
# An empty closure analyses no file, so only a deleted file's buffer can make a tuple stale here.
|
|
282
|
+
replayed = replayed_restored_run_level_rows?(closure: [], redemand_env: resolve)
|
|
283
|
+
record_hkt_scan_failure(hkt_scan_outcome(resolve.call)) unless replayed
|
|
265
284
|
prepare_rows
|
|
266
285
|
end
|
|
267
286
|
|
|
@@ -319,18 +338,46 @@ module Rigor
|
|
|
319
338
|
# of its own — and so no environment.
|
|
320
339
|
#
|
|
321
340
|
# @param closure — the files this run analyses; see {#stale_replayed_failure?}.
|
|
322
|
-
|
|
341
|
+
# @param redemand_env — resolves the environment {#redemand_stale_failures} re-derives a stale
|
|
342
|
+
# failure in; nil (the empty closure, which has no stale failure) re-derives nothing.
|
|
343
|
+
def replayed_restored_run_level_rows?(closure: nil, redemand_env: nil)
|
|
323
344
|
rows = @restored_run_level_rows
|
|
324
345
|
return false if rows.nil?
|
|
325
346
|
|
|
326
|
-
replayable = Array(rows.definition_build_failures).
|
|
347
|
+
stale, replayable = Array(rows.definition_build_failures).partition do |failure|
|
|
327
348
|
stale_replayed_failure?(failure, closure)
|
|
328
349
|
end
|
|
329
350
|
record_definition_build_failures(replayable)
|
|
351
|
+
redemand_stale_failures(stale, redemand_env)
|
|
330
352
|
record_hkt_scan_failure(rows.hkt_scan_failure)
|
|
331
353
|
true
|
|
332
354
|
end
|
|
333
355
|
|
|
356
|
+
# Issue #1929 — a stale replayed failure is RE-DERIVED, not dropped. Dropping it was right for the
|
|
357
|
+
# class whose collision the edit removed, and wrong for every class the edit left failing: the file
|
|
358
|
+
# that demanded such a class usually sits outside the recheck's closure (the edited file only
|
|
359
|
+
# contributes a conflicting buffer, such as a missing superclass every subclass inherits), so
|
|
360
|
+
# nothing in the recheck asks for it again, and the row lost classes a cold run reports. The class
|
|
361
|
+
# is asked for again in this run's environment ({Environment::RbsLoader#redemand_definition}), so
|
|
362
|
+
# it is reported exactly when it still fails, with the tuple the current signatures give it.
|
|
363
|
+
#
|
|
364
|
+
# A class this run's own analysis already reported is skipped; its tuple is current. The residue:
|
|
365
|
+
# a class whose only demand was in an edited file that no longer makes it, and which still fails,
|
|
366
|
+
# stays reported until a full run. The failure itself is real, unlike the dropped-collision case
|
|
367
|
+
# this replaced.
|
|
368
|
+
def redemand_stale_failures(stale, redemand_env)
|
|
369
|
+
return if stale.empty? || redemand_env.nil?
|
|
370
|
+
|
|
371
|
+
reported = @snapshots.definition_build_failures.to_set(&:first)
|
|
372
|
+
pending = stale.map(&:first).reject { |name| reported.include?(name) }
|
|
373
|
+
return if pending.empty?
|
|
374
|
+
|
|
375
|
+
loader = redemand_env.call&.rbs_loader
|
|
376
|
+
return if loader.nil?
|
|
377
|
+
|
|
378
|
+
record_definition_build_failures(pending.filter_map { |name| loader.redemand_definition(name) })
|
|
379
|
+
end
|
|
380
|
+
|
|
334
381
|
# A replayed definition-build failure the snapshot's own fingerprint cannot vouch for. The
|
|
335
382
|
# fingerprint digests the three signature sources it can read WITHOUT an environment — `sig:`,
|
|
336
383
|
# `Gemfile.lock` / `rbs_collection.lock.yaml`, and the configuration — but NOT the virtual RBS a
|
|
@@ -340,11 +387,11 @@ module Rigor
|
|
|
340
387
|
# closure, its analysis demands nothing that would re-derive the row, and the run reports a
|
|
341
388
|
# collision between files that no longer collide.
|
|
342
389
|
#
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
390
|
+
# Such a failure is not replayed as it stands; {#redemand_stale_failures} re-derives it instead
|
|
391
|
+
# (#1929), so the row asserts no collision the current signatures do not have. A failure whose
|
|
392
|
+
# buffers name no file this run touches still replays. A buffer whose file no longer exists is stale
|
|
393
|
+
# too: deleting an inline source a tuple names would otherwise replay the deleted declaration's
|
|
394
|
+
# failure, where a full run reports what the remaining signatures give.
|
|
348
395
|
def stale_replayed_failure?(failure, closure)
|
|
349
396
|
return false if closure.nil?
|
|
350
397
|
|
|
@@ -354,7 +401,10 @@ module Rigor
|
|
|
354
401
|
closure_paths = closure.to_set { |path| File.absolute_path(path.to_s) }
|
|
355
402
|
Array(buffers).any? do |name|
|
|
356
403
|
path = virtual_buffer_path(name)
|
|
357
|
-
|
|
404
|
+
next false if path.nil?
|
|
405
|
+
|
|
406
|
+
absolute = File.absolute_path(path)
|
|
407
|
+
closure_paths.include?(absolute) || !File.exist?(absolute)
|
|
358
408
|
end
|
|
359
409
|
end
|
|
360
410
|
|
|
@@ -870,7 +920,10 @@ module Rigor
|
|
|
870
920
|
merge_run_disclosures(session.drain_run_disclosures)
|
|
871
921
|
# The degraded slice was re-analysed on the parent session, so its dependency records live there,
|
|
872
922
|
# not in a child payload — fold them in alongside the successful children's.
|
|
873
|
-
|
|
923
|
+
if @record_dependencies
|
|
924
|
+
collect_dependencies(session.drain_dependencies, session.drain_analysis_reads,
|
|
925
|
+
session.drain_analysis_disclosures)
|
|
926
|
+
end
|
|
874
927
|
@collected_effects.merge!(session.drain_effects) if @record_effects
|
|
875
928
|
end
|
|
876
929
|
|
|
@@ -907,6 +960,8 @@ module Rigor
|
|
|
907
960
|
results = slice.to_h { |path| [path, session.analyze(path)] }
|
|
908
961
|
payload = { results: results, reporters: session.drain_reporters,
|
|
909
962
|
dependencies: session.drain_dependencies,
|
|
963
|
+
analysis_reads: session.drain_analysis_reads,
|
|
964
|
+
analysis_disclosures: session.drain_analysis_disclosures,
|
|
910
965
|
disclosures: session.drain_run_disclosures,
|
|
911
966
|
refusals: session.drain_trust_refusals }
|
|
912
967
|
# ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
|
|
@@ -945,7 +1000,10 @@ module Rigor
|
|
|
945
1000
|
merge_run_disclosures(payload.fetch(:disclosures, []))
|
|
946
1001
|
refusals = payload.fetch(:refusals, [])
|
|
947
1002
|
refusals_by_slice ? refusals_by_slice[child[:index]] = refusals : merge_trust_refusals(refusals)
|
|
948
|
-
|
|
1003
|
+
if @record_dependencies
|
|
1004
|
+
collect_dependencies(payload.fetch(:dependencies, {}), payload.fetch(:analysis_reads, {}),
|
|
1005
|
+
payload.fetch(:analysis_disclosures, {}))
|
|
1006
|
+
end
|
|
949
1007
|
@collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
|
|
950
1008
|
else
|
|
951
1009
|
degraded.concat(child[:slice])
|
|
@@ -954,6 +1012,14 @@ module Rigor
|
|
|
954
1012
|
degraded
|
|
955
1013
|
end
|
|
956
1014
|
|
|
1015
|
+
# ADR-46 / issues #1553, #2020 — fold one worker's dependency records, per-file plugin reads and per-file
|
|
1016
|
+
# run-scoped registrations.
|
|
1017
|
+
def collect_dependencies(dependencies, analysis_reads, analysis_disclosures)
|
|
1018
|
+
@collected_dependencies.merge!(dependencies)
|
|
1019
|
+
collected_analysis_reads.merge!(analysis_reads)
|
|
1020
|
+
collected_analysis_disclosures.merge!(analysis_disclosures)
|
|
1021
|
+
end
|
|
1022
|
+
|
|
957
1023
|
# @return the child's `{results:, reporters:}` payload, or nil when the child exited
|
|
958
1024
|
# abnormally or wrote no readable payload. `Marshal.load` is safe here: the blob was written by
|
|
959
1025
|
# our own forked child to a temp file we created.
|