rigortype 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +4 -3
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +207 -60
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +49 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -90,10 +90,48 @@ module Rigor
|
|
|
90
90
|
add_project_signatures(env, signature_paths)
|
|
91
91
|
add_virtual_rbs(env, virtual_rbs)
|
|
92
92
|
synthesize_missing_namespaces(env)
|
|
93
|
-
resolved = env
|
|
93
|
+
env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs)
|
|
94
94
|
stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# True when `content` parses as an RBS signature. {#virtual_rbs_collision_quarantined} uses this to
|
|
98
|
+
# tell a collision-dropped virtual entry (parses, but absent from the env) from a parse-failed one
|
|
99
|
+
# (the synthesizer's own WD6 skip, reported separately).
|
|
100
|
+
def parseable_rbs?(content)
|
|
101
|
+
::RBS::Parser.parse_signature(::RBS::Buffer.new(name: "(rigor: virtual parse check)", content: content))
|
|
102
|
+
true
|
|
103
|
+
rescue ::RBS::BaseError
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Backstop for a virtual-vs-anything `RBS::DuplicatedDeclarationError` that only materialises at
|
|
108
|
+
# `resolve_type_names` (which rebuilds the env from `sources`). {.add_virtual_rbs}'s transactional
|
|
109
|
+
# rescue already handles the add-time case — empirically everything on rbs 4.x — but the rbs gemspec
|
|
110
|
+
# range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an rbs-internal choice
|
|
111
|
+
# this code must not depend on. Resolution rule is the same as the add-time path: the explicit
|
|
112
|
+
# signature wins, the colliding VIRTUAL buffer is dropped whole (`RBS::Environment#unload`) and
|
|
113
|
+
# resolution retries; every pass removes at least one virtual buffer, so the loop is bounded by the
|
|
114
|
+
# virtual-entry count. A duplicate involving no virtual buffer (sig-vs-sig), or an env without
|
|
115
|
+
# `#unload` (rbs 3.x), re-raises into the existing one-warning degrade path.
|
|
116
|
+
#
|
|
117
|
+
# The dropped set is not returned: consumers recover it from the built env via
|
|
118
|
+
# {#virtual_rbs_collision_quarantined}, which also works on a cache HIT where this build never ran.
|
|
119
|
+
def resolve_quarantining_virtual_collisions(env, virtual_rbs)
|
|
120
|
+
virtual_names = virtual_rbs.to_set { |name, _content| name.to_s }
|
|
121
|
+
(virtual_names.size + 1).times do
|
|
122
|
+
return [env, env.resolve_type_names]
|
|
123
|
+
rescue ::RBS::DuplicatedDeclarationError => e
|
|
124
|
+
raise unless env.respond_to?(:unload)
|
|
125
|
+
|
|
126
|
+
culprits = e.decls.filter_map { |decl| decl.location&.buffer&.name }
|
|
127
|
+
.uniq.select { |name| virtual_names.include?(name) }
|
|
128
|
+
raise if culprits.empty?
|
|
129
|
+
|
|
130
|
+
env = env.unload(culprits)
|
|
131
|
+
end
|
|
132
|
+
[env, env.resolve_type_names]
|
|
133
|
+
end
|
|
134
|
+
|
|
97
135
|
# ADR-5 robustness, second tier. A project `signature_paths:` RBS that *references* a type no loaded
|
|
98
136
|
# signature declares — `def x: () -> DRb::DRbServer` when the `drb` RBS is not available, or a stale
|
|
99
137
|
# reference to its own removed `Textbringer::EditorError` — makes
|
|
@@ -356,9 +394,20 @@ module Rigor
|
|
|
356
394
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
357
395
|
add_parsed_decls(env, buffer, directives, decls)
|
|
358
396
|
rescue ::RBS::BaseError
|
|
359
|
-
# WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down
|
|
360
|
-
#
|
|
361
|
-
#
|
|
397
|
+
# WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down — for
|
|
398
|
+
# a parse error, skipping the entry is enough. But `RBS::Environment#add_source` appends to
|
|
399
|
+
# `env.sources` BEFORE inserting decls, so when the raise is a mid-insert
|
|
400
|
+
# `RBS::DuplicatedDeclarationError` (the entry declares a constant the project's own `sig/`
|
|
401
|
+
# already declares — the expected state for a project migrating between `sig/` and inline
|
|
402
|
+
# annotations, not an authoring error), the POISONED SOURCE is left behind, and
|
|
403
|
+
# `resolve_type_names` — which rebuilds the env from `sources` — re-raises the same error outside
|
|
404
|
+
# this rescue and collapses the WHOLE env to nil (measured on herb: 1,490 classes → 0, `require`
|
|
405
|
+
# itself stopped resolving, 74 false `call.unresolved-toplevel`). Make the skip transactional:
|
|
406
|
+
# drop the poisoned source. The explicit `.rbs` declaration wins — the spec keeps standalone
|
|
407
|
+
# `.rbs` files "the preferred place for complete type definitions" (`overview.md`) — and
|
|
408
|
+
# {#warn_about_virtual_rbs_collisions} names the dropped file. `sources` is the rbs 4.x shape;
|
|
409
|
+
# under the 3.x API this degrades to today's behaviour.
|
|
410
|
+
env.sources.reject! { |source| source.buffer.name == buffer.name } if env.respond_to?(:sources)
|
|
362
411
|
end
|
|
363
412
|
end
|
|
364
413
|
|
|
@@ -478,6 +527,55 @@ module Rigor
|
|
|
478
527
|
names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER)
|
|
479
528
|
end
|
|
480
529
|
|
|
530
|
+
# The project `signature_paths:` files that were QUARANTINED this run (they do not parse, so
|
|
531
|
+
# {RbsLoader.add_project_signatures} skipped them to keep the rest of the env alive), as
|
|
532
|
+
# `[absolute_path, first_error_line]` pairs. Memoised per loader: the detection re-parses only the user's
|
|
533
|
+
# own `sig/` set, but every consumer (the `rbs.coverage.quarantined-signature` diagnostic, `rigor doctor`,
|
|
534
|
+
# the stderr banner) reads it, and a cache HIT reaches it too — the env was built with the file already
|
|
535
|
+
# quarantined, so the condition is invisible in the cached env itself.
|
|
536
|
+
#
|
|
537
|
+
# @return [Array<Array(String, String)>] empty when every `signature_paths:` file parses.
|
|
538
|
+
def quarantined_signatures
|
|
539
|
+
@state[:quarantined] ||= self.class.quarantined_project_signatures(@signature_paths).freeze
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# The total RBS-environment build failure captured this run, or nil when the env built. Unlike
|
|
543
|
+
# {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived by
|
|
544
|
+
# re-parsing so a cache HIT reports it too — a total failure (typically `RBS::DuplicatedDeclarationError`:
|
|
545
|
+
# a `signature_paths:` entry redeclaring a constant/class Rigor's bundled RBS already ships) collapses the
|
|
546
|
+
# WHOLE env to nil. A failed build produces no cached success to hide behind (nothing is persisted, so
|
|
547
|
+
# every run re-attempts and re-raises), so this is captured directly in {#env}'s rescue rather than
|
|
548
|
+
# re-derived. Forcing `env` (any query does) populates it.
|
|
549
|
+
#
|
|
550
|
+
# @return [Array(String, String, Array<String>), nil] `[error_class_name, first_error_line,
|
|
551
|
+
# conflicting_buffer_names]`, or nil when the environment built successfully.
|
|
552
|
+
def env_build_failure
|
|
553
|
+
env unless @state[:env_loaded]
|
|
554
|
+
@state[:env_build_failure]
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Virtual (inline-synthesized) contributions dropped by the collision quarantine
|
|
558
|
+
# ({.resolve_quarantining_virtual_collisions}): buffer names absent from the built env even though the
|
|
559
|
+
# entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported
|
|
560
|
+
# through the synthesis reporter instead). Derived from the env rather than recorded during build —
|
|
561
|
+
# the {#quarantined_signatures} trick — so a cache HIT, which never runs the build, reports the same
|
|
562
|
+
# condition: the marshalled env simply lacks the dropped buffers.
|
|
563
|
+
#
|
|
564
|
+
# @return [Array<String>] virtual buffer names (source-file paths) whose contribution was dropped.
|
|
565
|
+
def virtual_rbs_collision_quarantined
|
|
566
|
+
@state[:virtual_rbs_collisions] ||= begin
|
|
567
|
+
built = @state[:env]
|
|
568
|
+
if built.nil? || @virtual_rbs.empty?
|
|
569
|
+
[].freeze
|
|
570
|
+
else
|
|
571
|
+
present = built.buffers.to_set(&:name)
|
|
572
|
+
@virtual_rbs.filter_map do |name, content|
|
|
573
|
+
name if !content.empty? && !present.include?(name) && self.class.parseable_rbs?(content)
|
|
574
|
+
end.freeze
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
481
579
|
# The referenced-but-undeclared types {.stub_missing_referenced_types} stubbed so the project classes
|
|
482
580
|
# that mention them could build (e.g. an unavailable `DRb::DRbServer`, or a stale
|
|
483
581
|
# `Textbringer::EditorError`). Recovered off the built env like {#synthesized_namespaces}, so it
|
|
@@ -970,8 +1068,10 @@ module Rigor
|
|
|
970
1068
|
@state[:env_loaded] = true
|
|
971
1069
|
@state[:env] = cache_store ? cached_env : build_env
|
|
972
1070
|
warn_about_quarantined_signatures
|
|
1071
|
+
warn_about_virtual_rbs_collisions
|
|
973
1072
|
@state[:env]
|
|
974
1073
|
rescue ::RBS::BaseError => e
|
|
1074
|
+
record_env_build_failure(e)
|
|
975
1075
|
warn_about_env_build_failure_once(e)
|
|
976
1076
|
@state[:env] = nil
|
|
977
1077
|
end
|
|
@@ -982,10 +1082,15 @@ module Rigor
|
|
|
982
1082
|
# is the visibility half of the fix: a shrinking diagnostic count must never be mistaken for a clean run
|
|
983
1083
|
# when it actually means "your sig/ stopped loading". No-op when `signature_paths:` is empty (the cost is
|
|
984
1084
|
# then a single empty-set check) or every file parses.
|
|
1085
|
+
#
|
|
1086
|
+
# `rigor check` ALSO reports this as the `rbs.coverage.quarantined-signature` diagnostic, which is what
|
|
1087
|
+
# reaches JSON / SARIF / CI annotations / the LSP. The banner is kept because the commands that build an
|
|
1088
|
+
# env WITHOUT producing a diagnostic stream (`coverage`, `sig-gen`) have no other channel, and a silently
|
|
1089
|
+
# degraded env is exactly what misleads there too.
|
|
985
1090
|
def warn_about_quarantined_signatures
|
|
986
1091
|
return if @state[:quarantine_warned]
|
|
987
1092
|
|
|
988
|
-
quarantined =
|
|
1093
|
+
quarantined = quarantined_signatures
|
|
989
1094
|
return if quarantined.empty?
|
|
990
1095
|
|
|
991
1096
|
@state[:quarantine_warned] = true
|
|
@@ -1002,6 +1107,60 @@ module Rigor
|
|
|
1002
1107
|
)
|
|
1003
1108
|
end
|
|
1004
1109
|
|
|
1110
|
+
# The collision twin of {#warn_about_quarantined_signatures}: name, once per run, the source files
|
|
1111
|
+
# whose inline-synthesized RBS was dropped because it collides with a declaration another signature
|
|
1112
|
+
# source already made ({.resolve_quarantining_virtual_collisions} — the explicit `.rbs` wins). Without
|
|
1113
|
+
# this the drop is silent, and "my `#:` annotation does nothing" has no visible cause. Reads the
|
|
1114
|
+
# derived {#virtual_rbs_collision_quarantined}, so a cache HIT warns identically.
|
|
1115
|
+
def warn_about_virtual_rbs_collisions
|
|
1116
|
+
return if @state[:virtual_collision_warned]
|
|
1117
|
+
|
|
1118
|
+
dropped = virtual_rbs_collision_quarantined
|
|
1119
|
+
return if dropped.empty?
|
|
1120
|
+
|
|
1121
|
+
@state[:virtual_collision_warned] = true
|
|
1122
|
+
listed = dropped.first(QUARANTINE_WARN_LIMIT)
|
|
1123
|
+
more = dropped.size - listed.size
|
|
1124
|
+
lines = listed.map { |name| " - #{name}" }
|
|
1125
|
+
lines << " … and #{more} more" if more.positive?
|
|
1126
|
+
warn(
|
|
1127
|
+
"rigor: dropped inline-RBS contribution(s) from #{dropped.size} file(s): they declare a
|
|
1128
|
+
" \
|
|
1129
|
+
"constant, alias, or global that another signature source (typically the project's own
|
|
1130
|
+
" \
|
|
1131
|
+
"`sig/`) already declares. The explicit `.rbs` declaration wins, so inline annotations in
|
|
1132
|
+
" \
|
|
1133
|
+
"these files do not bind. Remove the duplication from either side to restore them:
|
|
1134
|
+
" \
|
|
1135
|
+
"#{lines.join("\n")}"
|
|
1136
|
+
)
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
# Records the total RBS-environment build failure captured in {#env}'s rescue so the analysis layer can
|
|
1140
|
+
# surface it as the `rbs.coverage.environment-build-failed` diagnostic (the twin of
|
|
1141
|
+
# {#quarantined_signatures}: quarantine keeps the env alive minus one file, a total failure collapses the
|
|
1142
|
+
# WHOLE env to nil, so every type-of query degrades to `Dynamic[top]` and most rules stop firing). Stored
|
|
1143
|
+
# as `[error_class_name, first_error_line, conflicting_buffer_names]`. The buffer names are lifted off a
|
|
1144
|
+
# `RBS::DuplicatedDeclarationError#decls` — the typical failure, a `signature_paths:` entry redeclaring a
|
|
1145
|
+
# constant/class Rigor's bundled RBS already ships — so the diagnostic can name the colliding files.
|
|
1146
|
+
def record_env_build_failure(error)
|
|
1147
|
+
first_line = error.message.to_s.lines.first.to_s.strip
|
|
1148
|
+
@state[:env_build_failure] = [error.class.name, first_line, env_build_conflict_buffers(error)].freeze
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
# The buffer (file) names carried by the colliding declarations of a `RBS::DuplicatedDeclarationError`, so
|
|
1152
|
+
# the diagnostic names the conflicting signature files rather than guessing. A buffer name is a `String`
|
|
1153
|
+
# path for a project `signature_paths:` file and a `Pathname` for a bundled RBS file, so each is coerced
|
|
1154
|
+
# to `String`. Other RBS build errors carry no `.decls`; they yield an empty list and the diagnostic
|
|
1155
|
+
# falls back to the message's first line alone.
|
|
1156
|
+
def env_build_conflict_buffers(error)
|
|
1157
|
+
return [].freeze unless error.respond_to?(:decls)
|
|
1158
|
+
|
|
1159
|
+
error.decls.filter_map { |decl| decl.location&.buffer&.name }.map(&:to_s).uniq.freeze
|
|
1160
|
+
rescue ::RBS::BaseError, StandardError
|
|
1161
|
+
[].freeze
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1005
1164
|
def warn_about_env_build_failure_once(error)
|
|
1006
1165
|
return if @state[:env_build_warned]
|
|
1007
1166
|
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
|
+
require_relative "environment/default_libraries"
|
|
5
6
|
require_relative "environment/class_registry"
|
|
6
7
|
require_relative "environment/rbs_loader"
|
|
7
8
|
require_relative "environment/reflection"
|
|
8
9
|
require_relative "environment/reporters"
|
|
9
10
|
require_relative "environment/hkt_registry_holder"
|
|
10
11
|
require_relative "environment/constant_type_cache_holder"
|
|
12
|
+
require_relative "environment/missing_gem_constant_index"
|
|
11
13
|
require_relative "environment/bundle_sig_discovery"
|
|
12
14
|
require_relative "environment/lockfile_resolver"
|
|
13
15
|
require_relative "environment/rbs_collection_discovery"
|
|
16
|
+
require_relative "plugin/isolation"
|
|
14
17
|
require_relative "environment/rbs_coverage_report"
|
|
15
18
|
require_relative "inference/synthetic_method_index"
|
|
16
19
|
require_relative "inference/project_patched_methods"
|
|
@@ -31,31 +34,8 @@ module Rigor
|
|
|
31
34
|
DEFAULT_PROJECT_SIG_DIR = "sig"
|
|
32
35
|
private_constant :DEFAULT_PROJECT_SIG_DIR
|
|
33
36
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# accepted by `RBS::EnvironmentLoader#has_library?`; unknown libraries MUST fail-soft
|
|
37
|
-
# (`RbsLoader#build_env` already filters through `has_library?`). The default set covers the common
|
|
38
|
-
# stdlib surface a Ruby program is likely to import (`pathname`, `optparse`, `json`, `yaml`, `fileutils`,
|
|
39
|
-
# `tempfile`, `uri`, `logger`, `date`) plus the analyzer-adjacent gems shipping their own RBS in this
|
|
40
|
-
# bundle (`prism`, `rbs`). On hosts where one of these libraries is not installed, the loader silently
|
|
41
|
-
# drops it.
|
|
42
|
-
#
|
|
43
|
-
# Callers MAY add to the default by passing `libraries: %w[csv ...]`; the explicit list is appended to
|
|
44
|
-
# `DEFAULT_LIBRARIES` and de-duplicated. Callers that need a strictly RBS-core view MUST construct an
|
|
45
|
-
# `RbsLoader` directly instead of going through `for_project`.
|
|
46
|
-
DEFAULT_LIBRARIES = %w[
|
|
47
|
-
pathname optparse json yaml fileutils tempfile tmpdir
|
|
48
|
-
stringio forwardable digest securerandom
|
|
49
|
-
uri logger date
|
|
50
|
-
pp delegate observable abbrev find tsort singleton
|
|
51
|
-
shellwords benchmark base64 did_you_mean
|
|
52
|
-
monitor mutex_m timeout
|
|
53
|
-
open3 erb etc ipaddr bigdecimal bigdecimal-math
|
|
54
|
-
prettyprint random-formatter time open-uri resolv
|
|
55
|
-
csv pstore objspace io-console cgi cgi-escape
|
|
56
|
-
strscan
|
|
57
|
-
prism rbs
|
|
58
|
-
].freeze
|
|
37
|
+
# `DEFAULT_LIBRARIES` is defined in the light `environment/default_libraries.rb` (required below) so the
|
|
38
|
+
# ADR-87 WD4 boot-slimming probe can read it without loading the inference engine.
|
|
59
39
|
|
|
60
40
|
# ADR-72 — a Gemfile.lock gem name mapped to the opt-in plugin id that ships the SAME core-ext RBS. When
|
|
61
41
|
# that plugin is loaded the auto-overlay for the gem stands down, so the two never both declare the
|
|
@@ -84,7 +64,7 @@ module Rigor
|
|
|
84
64
|
rbs_extended_reporter: nil, boundary_cross_reporter: nil,
|
|
85
65
|
source_rbs_synthesis_reporter: nil,
|
|
86
66
|
synthetic_method_index: nil, project_patched_methods: nil,
|
|
87
|
-
hkt_registry: nil)
|
|
67
|
+
hkt_registry: nil, missing_rbs_gems: [], missing_rbs_bundle_path: nil)
|
|
88
68
|
@class_registry = class_registry
|
|
89
69
|
@rbs_loader = rbs_loader
|
|
90
70
|
@plugin_registry = plugin_registry
|
|
@@ -109,6 +89,12 @@ module Rigor
|
|
|
109
89
|
@hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
|
|
110
90
|
@hkt_registry_holder = HktRegistryHolder.new
|
|
111
91
|
@constant_type_cache = ConstantTypeCacheHolder.new
|
|
92
|
+
# ADR-82 WD9 — `[gem_name, version]` pairs for the locked gems with no resolvable RBS. The
|
|
93
|
+
# root-constant ownership index over them is built lazily (first unresolved constant read) so runs
|
|
94
|
+
# whose constants all resolve never pay the entry-file scan.
|
|
95
|
+
@missing_rbs_gems = missing_rbs_gems.freeze
|
|
96
|
+
@missing_rbs_bundle_path = missing_rbs_bundle_path
|
|
97
|
+
@missing_rbs_gem_constants_holder = HktRegistryHolder.new
|
|
112
98
|
@name_scope = build_name_scope
|
|
113
99
|
freeze
|
|
114
100
|
end
|
|
@@ -133,6 +119,21 @@ module Rigor
|
|
|
133
119
|
end
|
|
134
120
|
end
|
|
135
121
|
|
|
122
|
+
# ADR-82 WD9 — the gem name owning `root_constant_name`, when that gem is locked in the project's
|
|
123
|
+
# Gemfile.lock, ships no resolvable RBS, and its entry file declares the constant at top level. Nil for
|
|
124
|
+
# everything else — the caller then keeps the generic provenance cause. The index is built once, lazily,
|
|
125
|
+
# from RubyGems metadata + a Prism parse of each gem's entry file; no gem code runs (see
|
|
126
|
+
# {MissingGemConstantIndex}). Under the fork pool a worker that never meets an unresolved constant never
|
|
127
|
+
# builds it.
|
|
128
|
+
def missing_rbs_gem_owner(root_constant_name)
|
|
129
|
+
return nil if @missing_rbs_gems.empty?
|
|
130
|
+
|
|
131
|
+
index = @missing_rbs_gem_constants_holder.fetch do
|
|
132
|
+
MissingGemConstantIndex.build(@missing_rbs_gems, bundle_path: @missing_rbs_bundle_path)
|
|
133
|
+
end
|
|
134
|
+
index[root_constant_name.to_s]
|
|
135
|
+
end
|
|
136
|
+
|
|
136
137
|
# Backwards-compatible reporter accessors — every existing consumer (rbs_extended, method_dispatcher)
|
|
137
138
|
# calls these. The frozen `@reporters` container is mutable for slot reassignment via
|
|
138
139
|
# {#attach_reporters!} below.
|
|
@@ -220,6 +221,12 @@ module Rigor
|
|
|
220
221
|
auto_detect: bundler_auto_detect,
|
|
221
222
|
locked_gems: locked.empty? ? nil : locked
|
|
222
223
|
).map(&:to_s)
|
|
224
|
+
# ADR-82 WD9 — the resolved bundle root, so the missing-gem constant index reads each RBS-less gem's
|
|
225
|
+
# entry file from the TARGET's bundle (not rigor's own — see `MissingGemConstantIndex`). Resolved
|
|
226
|
+
# once here; passed through to the lazy index build.
|
|
227
|
+
bundle_root = resolve_target_bundle_root(
|
|
228
|
+
bundle_path: bundler_bundle_path, project_root: root, auto_detect: bundler_auto_detect
|
|
229
|
+
)
|
|
223
230
|
# O4 Layer 3 slice 2 — when `rbs collection install` has been run for the target project, parse the
|
|
224
231
|
# resulting `rbs_collection.lock.yaml` and feed each gem's `<collection_path>/<name>/<version>/`
|
|
225
232
|
# directory into `signature_paths:`. Stdlib-typed entries are skipped (already covered by
|
|
@@ -248,10 +255,9 @@ module Rigor
|
|
|
248
255
|
# diagnostic. Appended last so any RBS the project already supplies wins, and skipped for a gem whose
|
|
249
256
|
# opt-in plugin twin is loaded (no duplicate declaration). The paths ride in
|
|
250
257
|
# `loader_signature_paths`, so the env cache descriptor digests them for free.
|
|
251
|
-
overlay_paths =
|
|
252
|
-
locked: locked, default_libraries: merged_libraries,
|
|
253
|
-
|
|
254
|
-
plugin_registry: plugin_registry
|
|
258
|
+
missing_gems, overlay_paths = missing_gems_and_overlay_paths(
|
|
259
|
+
locked: locked, default_libraries: merged_libraries, bundle_sig_paths: gem_sig_paths,
|
|
260
|
+
rbs_collection_paths: collection_paths, plugin_registry: plugin_registry
|
|
255
261
|
)
|
|
256
262
|
loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths +
|
|
257
263
|
collection_paths + overlay_paths
|
|
@@ -286,7 +292,9 @@ module Rigor
|
|
|
286
292
|
source_rbs_synthesis_reporter: source_rbs_synthesis_reporter,
|
|
287
293
|
synthetic_method_index: synthetic_method_index,
|
|
288
294
|
project_patched_methods: project_patched_methods,
|
|
289
|
-
hkt_registry: Builtins::HktBuiltins.registry
|
|
295
|
+
hkt_registry: Builtins::HktBuiltins.registry,
|
|
296
|
+
missing_rbs_gems: missing_gems,
|
|
297
|
+
missing_rbs_bundle_path: bundle_root
|
|
290
298
|
)
|
|
291
299
|
end
|
|
292
300
|
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|
|
@@ -298,22 +306,42 @@ module Rigor
|
|
|
298
306
|
sig.directory? ? [sig] : []
|
|
299
307
|
end
|
|
300
308
|
|
|
301
|
-
# ADR-
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
def
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
# ADR-82 WD9 — the resolved bundle root for the missing-gem constant index, doubling (ADR-90) as the
|
|
310
|
+
# hand-off to the plugin isolation layer: ADR-39 target-library invocation (`Plugin::Inflector` →
|
|
311
|
+
# activesupport, the Rack status table) can then require the analyzed project's own locked gems when
|
|
312
|
+
# Rigor's host gem environment does not carry them — the standalone `gem install rigortype` case,
|
|
313
|
+
# where activesupport is deliberately not a runtime dependency.
|
|
314
|
+
def resolve_target_bundle_root(bundle_path:, project_root:, auto_detect:)
|
|
315
|
+
bundle_root = BundleSigDiscovery.resolve_bundle_path(
|
|
316
|
+
bundle_path: bundle_path, project_root: project_root, auto_detect: auto_detect
|
|
317
|
+
)&.to_s
|
|
318
|
+
Plugin::Isolation.target_bundle_root = bundle_root
|
|
319
|
+
bundle_root
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# The `:missing` classification (locked gems with no RBS through any resolution path), computed once
|
|
323
|
+
# and consumed twice: as `[gem_name, version]` pairs for the ADR-82 WD9 constant-ownership index, and
|
|
324
|
+
# as the eligible-gem set for the ADR-72 overlay resolution. Returns `[pairs, overlay_paths]`.
|
|
325
|
+
def missing_gems_and_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
|
|
326
|
+
rbs_collection_paths:, plugin_registry:)
|
|
327
|
+
return [[], []] if locked.empty?
|
|
328
|
+
|
|
329
|
+
rows = RbsCoverageReport.classify(
|
|
311
330
|
locked_gems: locked, default_libraries: default_libraries,
|
|
312
331
|
bundle_sig_paths: bundle_sig_paths, rbs_collection_paths: rbs_collection_paths
|
|
313
|
-
).select { |row| row.source == :missing }
|
|
332
|
+
).select { |row| row.source == :missing }
|
|
333
|
+
overlays = gem_overlay_paths(missing_gem_names: rows.map(&:gem_name), plugin_registry: plugin_registry)
|
|
334
|
+
[rows.map { |row| [row.gem_name, row.version] }, overlays]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# ADR-72 — resolve the bundled RBS overlay directories to load for this project. A gem is eligible when
|
|
338
|
+
# it is classified `:missing` (see {missing_rbs_gem_rows}) and its conflicting opt-in plugin (if any)
|
|
339
|
+
# is not loaded. Returns `[Pathname]`, deterministically ordered, or `[]` when no eligible gem.
|
|
340
|
+
def gem_overlay_paths(missing_gem_names:, plugin_registry:)
|
|
341
|
+
return [] if missing_gem_names.empty?
|
|
314
342
|
|
|
315
343
|
loaded_ids = plugin_registry ? plugin_registry.ids.to_set : Set.new
|
|
316
|
-
eligible =
|
|
344
|
+
eligible = missing_gem_names.reject do |gem_name|
|
|
317
345
|
plugin_id = GEM_OVERLAY_PLUGIN_IDS[gem_name]
|
|
318
346
|
plugin_id && loaded_ids.include?(plugin_id)
|
|
319
347
|
end.sort
|
|
@@ -8,6 +8,14 @@ module Rigor
|
|
|
8
8
|
# docs/type-specification/inference-budgets.md is not yet wired); counting how often each fires on a real
|
|
9
9
|
# project is the only way to see where inference actually stops.
|
|
10
10
|
#
|
|
11
|
+
# A second family of counters (the `MEMO_*` categories) profiles the ADR-57 user-method return memo
|
|
12
|
+
# (`ExpressionTyper#infer_user_method_return`) rather than a cutoff — inference entries, memo hits/misses,
|
|
13
|
+
# the split of non-stored results by reason, and body-evaluation (compute) entries — plus two
|
|
14
|
+
# per-signature distributions (body-eval count and distinct-memo-key count). This is the evidence for the
|
|
15
|
+
# next return-memo design slice: a finalization-aware taint gate (recover the `consult-tainted`
|
|
16
|
+
# non-stores) vs memo-key normalization (collapse arg-granularity thrash — a signature whose distinct-key
|
|
17
|
+
# count far exceeds its body-eval savings).
|
|
18
|
+
#
|
|
11
19
|
# Three categories, one per guard site:
|
|
12
20
|
#
|
|
13
21
|
# - {RECURSION_GUARD} — `ExpressionTyper#infer_user_method_return` detected a `(receiver, method)` cycle
|
|
@@ -44,9 +52,42 @@ module Rigor
|
|
|
44
52
|
# `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint.
|
|
45
53
|
BLOCK_WRITEBACK_CAP = :block_writeback_cap
|
|
46
54
|
|
|
55
|
+
# ADR-57 return-memo profile counters (not cutoffs — see the module doc). All bumped by
|
|
56
|
+
# `ExpressionTyper#infer_user_method_return` / `#compute_user_method_return`.
|
|
57
|
+
# - {MEMO_ENTRIES} — every `infer_user_method_return` entry (a user-method return inference).
|
|
58
|
+
# - {MEMO_HITS} / {MEMO_MISSES} — the memo was consulted (candidate frame) and the key was present /
|
|
59
|
+
# absent. Consults = hits + misses.
|
|
60
|
+
# - {MEMO_BODY_EVALS} — every `compute_user_method_return` entry (a body-evaluation compute; some are
|
|
61
|
+
# in-cycle re-entries that consult an ADR-55 summary rather than walk the body — those coincide with a
|
|
62
|
+
# `RECURSION_GUARD` hit). Body evals = misses + on-stack refusals (ADR-84 WD3 reduced candidacy to the
|
|
63
|
+
# on-stack check, so unroll-in-flight refusals are structurally gone).
|
|
64
|
+
# - {MEMO_REFUSE_ON_STACK} — the frame was not a memo candidate (bypassed the memo and computed): its
|
|
65
|
+
# plain `(receiver, method)` signature was already on the recursion guard stack.
|
|
66
|
+
# - {MEMO_REFUSE_UNROLL} — pre-ADR-84 blanket refusal (constant-arg unroll in flight). Kept in the table
|
|
67
|
+
# as the ADR-84 WD3 gate pin: structurally unreachable, must read ~0.
|
|
68
|
+
# - {MEMO_REFUSE_CONSULT_TAINTED} — a candidate frame computed a result but an ADR-55 fixpoint summary
|
|
69
|
+
# was *consulted* during the compute, so the result is a transient Kleene iterate and was NOT stored.
|
|
70
|
+
# - {MEMO_REFUSE_TRANSIENT} — ADR-84 WD3: a candidate frame's compute bracket saw a
|
|
71
|
+
# transient-machinery event (recursion-guard hit / unroll-fuel exhaustion / ADR-55 WD1 clamp /
|
|
72
|
+
# fixpoint-cap collapse — `ExpressionTyper#note_transient_fallback`) that referenced a stack frame
|
|
73
|
+
# BELOW the bracket's entry depth, so the ancestor context influenced the result and it was NOT
|
|
74
|
+
# stored. Own-machinery events (a recursive method's own fixpoint) do not refuse; top-of-stack
|
|
75
|
+
# computes are standalone by construction. Replaces the blanket unroll-in-flight exclusion.
|
|
76
|
+
MEMO_ENTRIES = :memo_entries
|
|
77
|
+
MEMO_HITS = :memo_hits
|
|
78
|
+
MEMO_MISSES = :memo_misses
|
|
79
|
+
MEMO_BODY_EVALS = :memo_body_evals
|
|
80
|
+
MEMO_REFUSE_ON_STACK = :memo_refuse_on_stack
|
|
81
|
+
MEMO_REFUSE_UNROLL = :memo_refuse_unroll
|
|
82
|
+
MEMO_REFUSE_CONSULT_TAINTED = :memo_refuse_consult_tainted
|
|
83
|
+
MEMO_REFUSE_TRANSIENT = :memo_refuse_transient
|
|
84
|
+
|
|
47
85
|
CATEGORIES = [
|
|
48
86
|
RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
|
|
49
|
-
RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP
|
|
87
|
+
RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP,
|
|
88
|
+
MEMO_ENTRIES, MEMO_HITS, MEMO_MISSES, MEMO_BODY_EVALS,
|
|
89
|
+
MEMO_REFUSE_ON_STACK, MEMO_REFUSE_UNROLL, MEMO_REFUSE_CONSULT_TAINTED,
|
|
90
|
+
MEMO_REFUSE_TRANSIENT
|
|
50
91
|
].freeze
|
|
51
92
|
|
|
52
93
|
# Distribution (histogram) categories — read-only observations of a value's size at a site, used to
|
|
@@ -55,12 +96,28 @@ module Rigor
|
|
|
55
96
|
# `Combinator.union` produces — the distribution the `union_size` budget default should be set from.
|
|
56
97
|
UNION_ARITY = :union_arity
|
|
57
98
|
|
|
58
|
-
|
|
99
|
+
# ADR-57 return-memo per-signature distributions — `{signature => count}` maps, NOT integer-size
|
|
100
|
+
# histograms, so `summarize` (percentiles over integer values) does not apply; read them with
|
|
101
|
+
# {distribution}. Both keyed by the `"Receiver#method"` plain signature.
|
|
102
|
+
# - {MEMO_BODY_EVAL_BY_SIGNATURE} — body-eval (compute) count per signature (via {observe}).
|
|
103
|
+
# - {MEMO_DISTINCT_KEY_BY_SIGNATURE} — distinct memo keys per signature (via {observe_distinct}), i.e.
|
|
104
|
+
# how many `(receiver, arg-type)` variants a signature was memoised under. A count far above the
|
|
105
|
+
# signature's body-eval savings is arg-granularity thrash — the key-normalization candidate.
|
|
106
|
+
MEMO_BODY_EVAL_BY_SIGNATURE = :memo_body_eval_by_signature
|
|
107
|
+
MEMO_DISTINCT_KEY_BY_SIGNATURE = :memo_distinct_key_by_signature
|
|
108
|
+
|
|
109
|
+
DISTRIBUTION_CATEGORIES = [
|
|
110
|
+
UNION_ARITY, MEMO_BODY_EVAL_BY_SIGNATURE, MEMO_DISTINCT_KEY_BY_SIGNATURE
|
|
111
|
+
].freeze
|
|
59
112
|
|
|
60
113
|
@enabled = !ENV["RIGOR_BUDGET_TRACE"].to_s.empty?
|
|
61
114
|
@mutex = Mutex.new
|
|
62
115
|
@counts = Hash.new(0)
|
|
63
116
|
@distributions = Hash.new { |h, k| h[k] = Hash.new(0) }
|
|
117
|
+
# Auxiliary de-duplication state for {observe_distinct}: category → Set of the `[bucket, member]`
|
|
118
|
+
# pairs already counted, so a repeated `(signature, memo-key)` observation is counted once. Only ever
|
|
119
|
+
# written when tracing is enabled; not part of the reported surface.
|
|
120
|
+
@distinct_seen = Hash.new { |h, k| h[k] = Set.new }
|
|
64
121
|
|
|
65
122
|
module_function
|
|
66
123
|
|
|
@@ -101,6 +158,23 @@ module Rigor
|
|
|
101
158
|
@mutex.synchronize { @distributions[category][value] += 1 }
|
|
102
159
|
end
|
|
103
160
|
|
|
161
|
+
# Records one observation of `member` into `category`'s `{bucket => count}` map, but only the FIRST time
|
|
162
|
+
# a given `(bucket, member)` pair is seen — so the resulting count per bucket is the number of DISTINCT
|
|
163
|
+
# members, not the number of observations. Used for the distinct-memo-key-per-signature distribution.
|
|
164
|
+
# No-op (one boolean check) when disabled.
|
|
165
|
+
def observe_distinct(category, bucket, member)
|
|
166
|
+
return unless @enabled
|
|
167
|
+
|
|
168
|
+
@mutex.synchronize do
|
|
169
|
+
seen = @distinct_seen[category]
|
|
170
|
+
pair = [bucket, member]
|
|
171
|
+
unless seen.include?(pair)
|
|
172
|
+
seen << pair
|
|
173
|
+
@distributions[category][bucket] += 1
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
104
178
|
# Frozen `{value => count}` histogram for a distribution category.
|
|
105
179
|
def distribution(category)
|
|
106
180
|
@mutex.synchronize { @distributions[category].dup.freeze }
|
|
@@ -137,6 +211,7 @@ module Rigor
|
|
|
137
211
|
@mutex.synchronize do
|
|
138
212
|
@counts.clear
|
|
139
213
|
@distributions.clear
|
|
214
|
+
@distinct_seen.clear
|
|
140
215
|
end
|
|
141
216
|
end
|
|
142
217
|
end
|
|
@@ -144,6 +144,16 @@ module Rigor
|
|
|
144
144
|
RANGE_EXTRA = %i[step].freeze
|
|
145
145
|
INTEGER_EXTRA = %i[times upto downto].freeze
|
|
146
146
|
|
|
147
|
+
# IO / File / StringIO line- and byte-iteration methods invoke the block immediately, once per line /
|
|
148
|
+
# byte / char, and never retain it — the same immediate-invocation contract as `Array#each`. Both the
|
|
149
|
+
# singleton `File.foreach` / `IO.foreach` (receiver `singleton(File)` → class name "File") and the
|
|
150
|
+
# instance `io.each_line` / `each_char` … forms resolve through the same class-name key. Without these,
|
|
151
|
+
# `File.foreach(path) { case … when … then flag = true when … then return true if flag end }` classifies
|
|
152
|
+
# `:unknown` and misses the loop-body re-narrowing, so a local written in one `when` arm reads its
|
|
153
|
+
# pre-loop value in a sibling arm and a guarding condition folds to a spurious constant.
|
|
154
|
+
IO_ITERATION = %i[each_line each each_byte each_char each_codepoint].freeze
|
|
155
|
+
IO_SINGLETON_ITERATION = %i[foreach].freeze
|
|
156
|
+
|
|
147
157
|
NON_ESCAPING = {
|
|
148
158
|
"Array" => (ENUMERABLE_NON_ESCAPING + ARRAY_EXTRA).freeze,
|
|
149
159
|
"Hash" => (ENUMERABLE_NON_ESCAPING + HASH_EXTRA).freeze,
|
|
@@ -151,7 +161,10 @@ module Rigor
|
|
|
151
161
|
"Set" => ENUMERABLE_NON_ESCAPING,
|
|
152
162
|
"Integer" => INTEGER_EXTRA,
|
|
153
163
|
"Enumerator" => ENUMERABLE_NON_ESCAPING,
|
|
154
|
-
"Enumerator::Lazy" => ENUMERABLE_NON_ESCAPING
|
|
164
|
+
"Enumerator::Lazy" => ENUMERABLE_NON_ESCAPING,
|
|
165
|
+
"IO" => (IO_ITERATION + IO_SINGLETON_ITERATION).freeze,
|
|
166
|
+
"File" => (IO_ITERATION + IO_SINGLETON_ITERATION).freeze,
|
|
167
|
+
"StringIO" => IO_ITERATION
|
|
155
168
|
}.freeze
|
|
156
169
|
|
|
157
170
|
# Methods that are documented to **retain** the block past the call. The block is stored or scheduled,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-85 WD3 — a lazy reference to a `Prism::DefNode` stored in a cross-file discovery table, in place of
|
|
6
|
+
# the live node itself. When an incremental recheck rebuilds the discovery index from cached per-file seed
|
|
7
|
+
# bundles (ADR-85 WD2), the `def_nodes` / `singleton_def_nodes` tables carry handles for every unchanged
|
|
8
|
+
# file's methods: the file was never parsed this run, so there is no live node to store. A handle records
|
|
9
|
+
# the three things needed to resolve or fingerprint it without the node:
|
|
10
|
+
#
|
|
11
|
+
# - `path` + `node_id` — the resolution key. The per-run parse memo ({Scope#resolve_def_handle}) parses the
|
|
12
|
+
# file once and returns the `Prism::DefNode` whose `node_id` matches (Prism `node_id` is stable across
|
|
13
|
+
# repeated parses of identical bytes). `name` is the cross-check: a resolved node whose `name` differs
|
|
14
|
+
# from the handle's forces a fresh walk (bundle/source skew that the digest gate should make impossible —
|
|
15
|
+
# conservative, never silent).
|
|
16
|
+
# - `fingerprint` — the SHA-256 of the def's source slice, captured when the bundle was built. This lets
|
|
17
|
+
# `Runner#symbol_fingerprints` read the change-detection fingerprint (ADR-46 slice 4) off the handle
|
|
18
|
+
# without re-parsing the file — the one value-deref consumer besides the three accessor choke points.
|
|
19
|
+
#
|
|
20
|
+
# Marshal-clean by construction (Integer + Strings), so it rides the `IncrementalSnapshot` blob directly.
|
|
21
|
+
DefHandle = Data.define(:path, :node_id, :name, :fingerprint)
|
|
22
|
+
end
|
|
23
|
+
end
|