rigortype 0.2.9 → 0.3.1
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/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- 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 +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- 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 +78 -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 +344 -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_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- 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 +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- 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/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- 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 +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- 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 +361 -113
- 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 +17 -1
- 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 +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- 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 +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- 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/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- 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/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- 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 +207 -49
- 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-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- 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-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 +3 -1
- 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/routes_parser.rb +7 -5
- 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/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- 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 +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
data/lib/rigor/reflection.rb
CHANGED
|
@@ -93,6 +93,11 @@ module Rigor
|
|
|
93
93
|
# Returns the type of the named constant. Joins in-source constants (recorded by
|
|
94
94
|
# `ScopeIndexer`) and RBS-side constants. In-source wins on collision because the user's
|
|
95
95
|
# source is the authoritative declaration.
|
|
96
|
+
#
|
|
97
|
+
# This is the flat lookup keyed on the literal `constant_name`. Callers that need Ruby's
|
|
98
|
+
# lexical constant resolution (walking the enclosing class path, and folding in registry
|
|
99
|
+
# classes / source-discovered classes as `Singleton` / class types) use
|
|
100
|
+
# {.resolve_constant_type} instead.
|
|
96
101
|
def constant_type_for(constant_name, scope: Scope.empty)
|
|
97
102
|
key = constant_name.to_s
|
|
98
103
|
in_source = scope.in_source_constants[key]
|
|
@@ -101,6 +106,65 @@ module Rigor
|
|
|
101
106
|
scope.environment.constant_for_name(constant_name)
|
|
102
107
|
end
|
|
103
108
|
|
|
109
|
+
# Resolves a constant *reference* to its type through Ruby's lexical constant lookup: the
|
|
110
|
+
# most-qualified candidate first (the enclosing class path joined to `name`), then
|
|
111
|
+
# progressively less-qualified, then the bare `name`. Each candidate consults, in order,
|
|
112
|
+
# the class registry (yielding a `Singleton[C]`), source-discovered classes, in-source
|
|
113
|
+
# value constants, and finally RBS-side constants — in-source value constants winning over
|
|
114
|
+
# RBS because the user's source is authoritative for its own constants. Returns the matched
|
|
115
|
+
# `Rigor::Type`, or nil when no source knows the constant.
|
|
116
|
+
#
|
|
117
|
+
# This is the shared owner of the lexical-constant resolution: `Inference::ExpressionTyper`
|
|
118
|
+
# reads it to type a constant read, and `Inference::Narrowing` reads it to recognise a
|
|
119
|
+
# value-pinned `Constant[Regexp]` match-predicate operand.
|
|
120
|
+
def resolve_constant_type(name, scope: Scope.empty)
|
|
121
|
+
env = scope.environment
|
|
122
|
+
discovered = scope.discovered_classes
|
|
123
|
+
in_source = scope.in_source_constants
|
|
124
|
+
lexical_constant_candidates(name, scope: scope).each do |candidate|
|
|
125
|
+
singleton = env.singleton_for_name(candidate)
|
|
126
|
+
return singleton if singleton
|
|
127
|
+
|
|
128
|
+
in_source_class = discovered[candidate]
|
|
129
|
+
return in_source_class if in_source_class
|
|
130
|
+
|
|
131
|
+
# In-source value-bearing constants take precedence over RBS constant decls because
|
|
132
|
+
# user code is the authoritative source for its own constants.
|
|
133
|
+
in_source_value = in_source[candidate]
|
|
134
|
+
return in_source_value if in_source_value
|
|
135
|
+
|
|
136
|
+
value = env.constant_for_name(candidate)
|
|
137
|
+
return value if value
|
|
138
|
+
end
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The candidate qualified names to try, in Ruby's lexical order: most-qualified first (the
|
|
143
|
+
# enclosing class path joined to `name`), then progressively less-qualified, then the bare
|
|
144
|
+
# `name`. A top-level scope (no `self_type`) yields only `[name]`.
|
|
145
|
+
def lexical_constant_candidates(name, scope: Scope.empty)
|
|
146
|
+
prefix = enclosing_class_path(scope)
|
|
147
|
+
candidates = []
|
|
148
|
+
while prefix && !prefix.empty?
|
|
149
|
+
candidates << "#{prefix}::#{name}"
|
|
150
|
+
idx = prefix.rindex("::")
|
|
151
|
+
prefix = idx ? prefix[0, idx] : nil
|
|
152
|
+
end
|
|
153
|
+
candidates << name
|
|
154
|
+
candidates
|
|
155
|
+
end
|
|
156
|
+
private_class_method :lexical_constant_candidates
|
|
157
|
+
|
|
158
|
+
# Pulls the enclosing qualified class name out of `scope.self_type` when one is set.
|
|
159
|
+
# `Nominal[T]` and `Singleton[T]` both expose `class_name`. Returns nil at the top level.
|
|
160
|
+
def enclosing_class_path(scope)
|
|
161
|
+
st = scope.self_type
|
|
162
|
+
case st
|
|
163
|
+
when Type::Nominal, Type::Singleton then st.class_name
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
private_class_method :enclosing_class_path
|
|
167
|
+
|
|
104
168
|
# Returns the RBS `RBS::Definition::Method` for the instance method, or nil when the
|
|
105
169
|
# class or method is not in RBS. The source-side discovered-method facts are reachable
|
|
106
170
|
# through {.discovered_method?}; a future slice will unify the two under a
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# Process-level runtime concerns that sit beside the analyzer rather than
|
|
5
|
+
# inside it. Currently just deferred YJIT enablement.
|
|
6
|
+
module Runtime
|
|
7
|
+
# Deferred YJIT enablement for the long-running analysis commands.
|
|
8
|
+
#
|
|
9
|
+
# Ruby 3.3+ ships YJIT but leaves it off unless the process opts in
|
|
10
|
+
# (`--yjit`, `RUBY_YJIT_ENABLE=1`, or `RubyVM::YJIT.enable`). Enabling it
|
|
11
|
+
# unconditionally at boot is a net LOSS on short `rigor check` / `coverage`
|
|
12
|
+
# runs: the JIT compile cost is paid up front, but a run that finishes in a
|
|
13
|
+
# couple of seconds ends before it amortizes. Measured cold A/B (2026-07-13,
|
|
14
|
+
# Ruby 4.0.5): mastodon `app/models` 4.0s -> 5.0s (0.80x), kramdown `lib`
|
|
15
|
+
# 1.7s -> 2.55s (0.66x), dependabot 1.6s -> 2.34s (0.68x). Long runs win big
|
|
16
|
+
# in the same measurement: mastodon `app`+`lib` 25.2s -> 14.9s (1.69x),
|
|
17
|
+
# redmine `app/models` 8.6s -> 5.5s (1.56x). Allocations and diagnostics are
|
|
18
|
+
# byte-identical under YJIT — the tradeoff is purely wall-time.
|
|
19
|
+
#
|
|
20
|
+
# The resolution is to enable YJIT only once a run has proven long enough to
|
|
21
|
+
# amortize the compile cost. {enable_after} spawns one daemon thread that
|
|
22
|
+
# sleeps the deadline then enables: a run that finishes before the deadline
|
|
23
|
+
# never pays JIT compile (the sub-deadline penalty zone is avoided by
|
|
24
|
+
# construction), while a long run enables mid-flight and JITs the remaining
|
|
25
|
+
# — dominant — work. Long-lived servers (`rigor lsp` / `rigor mcp`) instead
|
|
26
|
+
# enable at boot via {enable_now}, since they always run long.
|
|
27
|
+
#
|
|
28
|
+
# Opt-out: `RIGOR_DISABLE_YJIT=1` makes every entry point a no-op (the pin
|
|
29
|
+
# the byte-identical-diagnostics gate uses). If YJIT is already enabled (the
|
|
30
|
+
# user passed `--yjit` / `RUBY_YJIT_ENABLE=1`) or is unavailable (a build
|
|
31
|
+
# without YJIT, or a non-MRI Ruby), both entry points are no-ops too.
|
|
32
|
+
module Jit
|
|
33
|
+
# Opt-out switch: `RIGOR_DISABLE_YJIT=1` disables both entry points.
|
|
34
|
+
DISABLE_ENV = "RIGOR_DISABLE_YJIT"
|
|
35
|
+
|
|
36
|
+
# Advanced / calibration override of the {enable_after} deadline, in
|
|
37
|
+
# float seconds. Unset (or unparseable / negative) falls back to
|
|
38
|
+
# {DEFAULT_DEADLINE_SECONDS}. Exposed so the deadline can be swept during
|
|
39
|
+
# calibration and tuned per-project without an engine change.
|
|
40
|
+
DEADLINE_ENV = "RIGOR_YJIT_DEADLINE"
|
|
41
|
+
|
|
42
|
+
# The amortization deadline for {enable_after}, in seconds.
|
|
43
|
+
#
|
|
44
|
+
# Calibrated by interleaved cold A/B measurement on 2026-07-13 (Ruby
|
|
45
|
+
# 4.0.5, arm64-darwin25). Enabling YJIT mid-run costs a ~1s compile burst,
|
|
46
|
+
# so a run whose length is only ~0-1s longer than the deadline pays the
|
|
47
|
+
# burst without time to amortize and LOSES (measured: `app/models` at a
|
|
48
|
+
# 3.0s deadline regressed 4.14s -> 4.68s). The short-run cases cluster at
|
|
49
|
+
# <=1.7s (kramdown / dependabot) and ~4.0-4.9s (mastodon `app/models` ~4.1s,
|
|
50
|
+
# mail ~4.9s), with the next case at redmine ~7s. 5.0s parks that danger
|
|
51
|
+
# zone in the empty 5-6s gap: every case <=4.9s finishes before the
|
|
52
|
+
# deadline fires (guaranteed no-YJIT parity — the sleeping thread is killed
|
|
53
|
+
# at exit), while runs long enough to pay off (>=~15s: gitlab, mastodon
|
|
54
|
+
# `app`+`lib`) enable mid-flight and JIT their dominant tail — the mastodon
|
|
55
|
+
# `app`+`lib` win is preserved ~100% (25.4s -> 15.1s, vs 15.1s always-on).
|
|
56
|
+
DEFAULT_DEADLINE_SECONDS = 5.0
|
|
57
|
+
|
|
58
|
+
module_function
|
|
59
|
+
|
|
60
|
+
# Enables YJIT now, unless opted out, already enabled, or unavailable.
|
|
61
|
+
#
|
|
62
|
+
# An imperative command, not a query — the boolean reports whether this
|
|
63
|
+
# call is what enabled YJIT (so the `?`-suffix predicate convention does
|
|
64
|
+
# not apply; hence the Naming/PredicateMethod exemption).
|
|
65
|
+
#
|
|
66
|
+
# @return [Boolean] true if this call enabled YJIT; false if it was a
|
|
67
|
+
# no-op (unavailable / opted out / already enabled).
|
|
68
|
+
def enable_now # rubocop:disable Naming/PredicateMethod
|
|
69
|
+
return false unless available?
|
|
70
|
+
return false if disabled?
|
|
71
|
+
return false if RubyVM::YJIT.enabled?
|
|
72
|
+
|
|
73
|
+
RubyVM::YJIT.enable
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Spawns one background thread that sleeps `seconds` then calls
|
|
78
|
+
# {enable_now}. The thread is a daemon in effect — Ruby kills all live
|
|
79
|
+
# threads when the main thread exits — so a run that finishes before the
|
|
80
|
+
# deadline simply never enables YJIT, which is the point.
|
|
81
|
+
#
|
|
82
|
+
# Returns nil (and spawns no thread) when the feature is already a no-op
|
|
83
|
+
# up front (unavailable / opted out / already enabled), so callers never
|
|
84
|
+
# pay for a thread that could only no-op.
|
|
85
|
+
#
|
|
86
|
+
# @param seconds [Numeric] the amortization deadline.
|
|
87
|
+
# @return [Thread, nil] the deadline thread, or nil when no-op up front.
|
|
88
|
+
def enable_after(seconds)
|
|
89
|
+
return nil unless available?
|
|
90
|
+
return nil if disabled?
|
|
91
|
+
return nil if RubyVM::YJIT.enabled?
|
|
92
|
+
|
|
93
|
+
thread = Thread.new do
|
|
94
|
+
sleep(seconds)
|
|
95
|
+
enable_now
|
|
96
|
+
end
|
|
97
|
+
# A background perf optimization must never spill a backtrace onto the
|
|
98
|
+
# run's stderr; on any unexpected failure, stay silent and leave YJIT
|
|
99
|
+
# off. The deadline sleep gives ample time to set this before the body
|
|
100
|
+
# could raise.
|
|
101
|
+
thread.report_on_exception = false
|
|
102
|
+
thread
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The resolved {enable_after} deadline: {DEADLINE_ENV} when it parses to a
|
|
106
|
+
# non-negative float, else {DEFAULT_DEADLINE_SECONDS}.
|
|
107
|
+
#
|
|
108
|
+
# @return [Numeric]
|
|
109
|
+
def deadline_seconds
|
|
110
|
+
raw = ENV.fetch(DEADLINE_ENV, nil)
|
|
111
|
+
return DEFAULT_DEADLINE_SECONDS if raw.nil? || raw.empty?
|
|
112
|
+
|
|
113
|
+
value = Float(raw, exception: false)
|
|
114
|
+
value && value >= 0 ? value : DEFAULT_DEADLINE_SECONDS
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [Boolean] whether this Ruby exposes `RubyVM::YJIT.enable`.
|
|
118
|
+
def available?
|
|
119
|
+
defined?(RubyVM::YJIT.enable) ? true : false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Boolean] whether the opt-out env switch is set.
|
|
123
|
+
def disabled?
|
|
124
|
+
ENV[DISABLE_ENV] == "1"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -21,13 +21,15 @@ module Rigor
|
|
|
21
21
|
:discovered_def_nodes,
|
|
22
22
|
:discovered_singleton_def_nodes,
|
|
23
23
|
:discovered_def_sources,
|
|
24
|
+
:discovered_singleton_def_sources,
|
|
24
25
|
:discovered_method_visibilities,
|
|
25
26
|
:discovered_superclasses,
|
|
26
27
|
:discovered_includes,
|
|
27
28
|
:discovered_class_sources,
|
|
28
29
|
:data_member_layouts,
|
|
29
30
|
:struct_member_layouts,
|
|
30
|
-
:param_inferred_types
|
|
31
|
+
:param_inferred_types,
|
|
32
|
+
:run_generation
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
class DiscoveryIndex
|
|
@@ -48,6 +50,7 @@ module Rigor
|
|
|
48
50
|
discovered_def_nodes: EMPTY_TABLE,
|
|
49
51
|
discovered_singleton_def_nodes: EMPTY_TABLE,
|
|
50
52
|
discovered_def_sources: EMPTY_TABLE,
|
|
53
|
+
discovered_singleton_def_sources: EMPTY_TABLE,
|
|
51
54
|
discovered_method_visibilities: EMPTY_TABLE,
|
|
52
55
|
discovered_superclasses: EMPTY_TABLE,
|
|
53
56
|
discovered_includes: EMPTY_TABLE,
|
|
@@ -60,7 +63,14 @@ module Rigor
|
|
|
60
63
|
# `{param_name(Symbol) => Rigor::Type}` map of the union of resolved call-site argument types. Empty on
|
|
61
64
|
# every normal run; only the `coverage --protection` collection pass populates it today, so a `check` run
|
|
62
65
|
# leaves it empty and seeds nothing (byte-identical).
|
|
63
|
-
param_inferred_types: EMPTY_TABLE
|
|
66
|
+
param_inferred_types: EMPTY_TABLE,
|
|
67
|
+
# ADR-84 WD2 — the run-scope identity token `Analysis::Runner#run_analysis` mints per run (a frozen
|
|
68
|
+
# bare Object) and seeds through `project_scope_seed_tables`. The user-method return memo keys its
|
|
69
|
+
# bucket on this token's identity so hits cross consumer-file boundaries within one run but never
|
|
70
|
+
# cross a run boundary (LSP / ADR-62 warm-loop re-runs land in a fresh bucket). Nil on scopes that
|
|
71
|
+
# never see the runner seed (single-file probes, `run_source` before the seed applies): the memo
|
|
72
|
+
# falls back to today's per-file `discovered_def_nodes` identity for those.
|
|
73
|
+
run_generation: nil
|
|
64
74
|
)
|
|
65
75
|
end
|
|
66
76
|
end
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "analysis/dependency_recorder"
|
|
|
8
8
|
require_relative "inference/expression_typer"
|
|
9
9
|
require_relative "inference/flow_tracer"
|
|
10
10
|
require_relative "inference/statement_evaluator"
|
|
11
|
+
require_relative "inference/def_node_resolver"
|
|
11
12
|
|
|
12
13
|
module Rigor
|
|
13
14
|
# Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State
|
|
@@ -22,7 +23,8 @@ module Rigor
|
|
|
22
23
|
:indexed_narrowings, :method_chain_narrowings,
|
|
23
24
|
:declaration_sourced,
|
|
24
25
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
25
|
-
:dynamic_origins, :local_origins, :ivar_origins
|
|
26
|
+
:dynamic_origins, :local_origins, :ivar_origins,
|
|
27
|
+
:void_origins
|
|
26
28
|
|
|
27
29
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
28
30
|
# reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
|
|
@@ -41,6 +43,7 @@ module Rigor
|
|
|
41
43
|
def discovered_def_nodes = @discovery.discovered_def_nodes
|
|
42
44
|
def discovered_singleton_def_nodes = @discovery.discovered_singleton_def_nodes
|
|
43
45
|
def discovered_def_sources = @discovery.discovered_def_sources
|
|
46
|
+
def discovered_singleton_def_sources = @discovery.discovered_singleton_def_sources
|
|
44
47
|
def discovered_method_visibilities = @discovery.discovered_method_visibilities
|
|
45
48
|
def discovered_superclasses = @discovery.discovered_superclasses
|
|
46
49
|
def discovered_includes = @discovery.discovered_includes
|
|
@@ -52,6 +55,9 @@ module Rigor
|
|
|
52
55
|
# call-site argument types (precision-additive; an RBS-declared parameter always wins). Empty unless a
|
|
53
56
|
# collection pass seeded it.
|
|
54
57
|
def param_inferred_types = @discovery.param_inferred_types
|
|
58
|
+
# ADR-84 WD2 — the per-run identity token the user-method return memo buckets on (nil outside runner-seeded
|
|
59
|
+
# scopes; the memo then falls back to the per-file `discovered_def_nodes` identity).
|
|
60
|
+
def run_generation = @discovery.run_generation
|
|
55
61
|
|
|
56
62
|
# Narrowing key for an indexed read `receiver[key]` where both the receiver and the key are stable enough to
|
|
57
63
|
# address. The value of the map at this key is the narrowed type the next read at the same address MUST
|
|
@@ -117,6 +123,16 @@ module Rigor
|
|
|
117
123
|
self
|
|
118
124
|
end
|
|
119
125
|
|
|
126
|
+
# ADR-100 WD3 — records that the value introduced at `node` (a call node) is a `top` recovered from an
|
|
127
|
+
# author-declared `-> void` return, keyed by the origin site (`origin`, an {Inference::VoidOrigin}). The
|
|
128
|
+
# value-context check rule `static.value-use.void` consumes this table. Mirrors {#record_dynamic_origin}
|
|
129
|
+
# exactly: identity-keyed advisory metadata, mutated in place on the shared table (threaded by reference
|
|
130
|
+
# through `#join` / `#rebuild`), excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
|
|
131
|
+
def record_void_origin(node, origin)
|
|
132
|
+
@void_origins[node] = origin
|
|
133
|
+
self
|
|
134
|
+
end
|
|
135
|
+
|
|
120
136
|
def initialize(
|
|
121
137
|
environment:, locals:,
|
|
122
138
|
fact_store: Analysis::FactStore.empty,
|
|
@@ -132,7 +148,8 @@ module Rigor
|
|
|
132
148
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
133
149
|
dynamic_origins: {}.compare_by_identity,
|
|
134
150
|
local_origins: EMPTY_ORIGINS,
|
|
135
|
-
ivar_origins: EMPTY_ORIGINS
|
|
151
|
+
ivar_origins: EMPTY_ORIGINS,
|
|
152
|
+
void_origins: {}.compare_by_identity
|
|
136
153
|
)
|
|
137
154
|
@environment = environment
|
|
138
155
|
@locals = locals
|
|
@@ -150,6 +167,7 @@ module Rigor
|
|
|
150
167
|
@dynamic_origins = dynamic_origins
|
|
151
168
|
@local_origins = local_origins
|
|
152
169
|
@ivar_origins = ivar_origins
|
|
170
|
+
@void_origins = void_origins
|
|
153
171
|
freeze
|
|
154
172
|
end
|
|
155
173
|
|
|
@@ -294,6 +312,39 @@ module Rigor
|
|
|
294
312
|
@declaration_sourced.include?([kind.to_sym, name.to_sym])
|
|
295
313
|
end
|
|
296
314
|
|
|
315
|
+
# ADR-67 WD6b — stamp the "inferred, not declared" provenance mark on a parameter local seeded from the
|
|
316
|
+
# call-site parameter-inference table ({Inference::ParameterInferenceCollector}). Rides the ADR-58 WD1
|
|
317
|
+
# declaration-sourced side-mark machinery under a distinct `:inferred_param` kind (never a carrier field,
|
|
318
|
+
# so the displayed type is unchanged) so the negative in-body rules can decline on a receiver / argument
|
|
319
|
+
# whose type is an open-call-site *lower bound* — firing against a lower bound is a false positive by
|
|
320
|
+
# construction (the ADR-67 WD1 reasoning at the parameter boundary, carried one hop into the body). The
|
|
321
|
+
# distinct kind keeps the inferred-param sites separable from ADR-58's ivar-copy `:local` mark, which a
|
|
322
|
+
# later un-guarding slice (WD6b) needs. `with_local` drops it on any flow-live rewrite of the local
|
|
323
|
+
# (`drop_local_declaration_marks`), so only the pristine parameter binding carries it.
|
|
324
|
+
def with_inferred_param_mark(name)
|
|
325
|
+
rebuild(declaration_sourced: add_declaration_sourced(:inferred_param, name))
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# ADR-67 WD6b — explicitly clear the inferred-parameter taint on `name`. The mark is deliberately STICKY:
|
|
329
|
+
# `with_local` (used by both narrowing and reassignment) does NOT drop it, so it survives the narrowing /
|
|
330
|
+
# join transitions between a lower-bound value's definition and its use (`v = param[i]-1; if 0<=v and v<n`
|
|
331
|
+
# — the `and` narrows `v` between the two comparisons). It is cleared only at a genuine source-level local
|
|
332
|
+
# write whose RHS does not derive from an inferred parameter ({StatementEvaluator#eval_local_write}), so a
|
|
333
|
+
# local rebound to an independent value stops being treated as a lower bound. Over-retention (a mark that
|
|
334
|
+
# outlives a rebind the write-path did not catch) only ever suppresses a diagnostic — the FP-safe
|
|
335
|
+
# direction — so stickiness is the conservative choice. Zero-alloc when no mark is present.
|
|
336
|
+
def without_inferred_param_mark(name)
|
|
337
|
+
dropped = drop_declaration_sourced_for(:inferred_param, name)
|
|
338
|
+
dropped.equal?(@declaration_sourced) ? self : rebuild(declaration_sourced: dropped)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# ADR-67 WD6b — true when `name`'s local binding is a pristine inferred parameter (the call-site union
|
|
342
|
+
# seeded at method entry, untouched by a flow-live write). The guard predicate the negative in-body rules
|
|
343
|
+
# consult.
|
|
344
|
+
def inferred_param?(name)
|
|
345
|
+
@declaration_sourced.include?([:inferred_param, name.to_sym])
|
|
346
|
+
end
|
|
347
|
+
|
|
297
348
|
def with_cvar(name, type)
|
|
298
349
|
rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
|
|
299
350
|
end
|
|
@@ -366,9 +417,12 @@ module Rigor
|
|
|
366
417
|
# inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.
|
|
367
418
|
def user_def_for(class_name, method_name)
|
|
368
419
|
table = @discovery.discovered_def_nodes[class_name.to_s]
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
node
|
|
420
|
+
# ADR-85 WD3 — the value is either a live `Prism::DefNode` (cold / re-walked file) or a `DefHandle`
|
|
421
|
+
# (unchanged file, bundle-rebuilt index). Dependency recording keys on the table's PRESENCE (both are
|
|
422
|
+
# truthy), so it is sound regardless of resolution; only the returned node is resolved lazily.
|
|
423
|
+
entry = table && table[method_name.to_sym]
|
|
424
|
+
record_cross_file_method(class_name, method_name, entry) if Analysis::DependencyRecorder.active?
|
|
425
|
+
Inference::DefNodeResolver.resolve(entry)
|
|
372
426
|
end
|
|
373
427
|
|
|
374
428
|
# Module-singleton call resolution (ADR-57 follow-up) — companion of {#user_def_for} for SINGLETON-side defs
|
|
@@ -379,25 +433,28 @@ module Rigor
|
|
|
379
433
|
# the same cross-file dependency edge as the instance path (ADR-46).
|
|
380
434
|
def singleton_def_for(class_name, method_name)
|
|
381
435
|
table = @discovery.discovered_singleton_def_nodes[class_name.to_s]
|
|
382
|
-
|
|
383
|
-
record_cross_file_method(class_name, method_name,
|
|
384
|
-
|
|
436
|
+
entry = table && table[method_name.to_sym] # live node or DefHandle (ADR-85 WD3)
|
|
437
|
+
record_cross_file_method(class_name, method_name, entry, singleton: true) if Analysis::DependencyRecorder.active?
|
|
438
|
+
Inference::DefNodeResolver.resolve(entry)
|
|
385
439
|
end
|
|
386
440
|
|
|
387
441
|
# ADR-46 slice 1 — note the cross-file dependency this resolution creates: the file defining
|
|
388
442
|
# `class_name#method_name` (the consumer's analysis reads its body via `infer_user_method_return`), or, when
|
|
389
443
|
# unresolved, a negative edge so a later definition re-checks the consumer. Gated on the recorder being
|
|
390
|
-
# active — no-op on a normal run.
|
|
391
|
-
|
|
444
|
+
# active — no-op on a normal run. `singleton:` selects the singleton-side source table + a `"Class.method"`
|
|
445
|
+
# symbol key (vs the instance `"Class#method"`), so a class/singleton-method body edit produces a changed
|
|
446
|
+
# symbol pair and scopes to the method's call sites the same way an instance-method edit does — the source
|
|
447
|
+
# site is read from `discovered_singleton_def_sources`, the mirror the ScopeIndexer now records (ADR-46
|
|
448
|
+
# slice 4 singleton extension). Both keys share the format `Runner#symbol_fingerprints` emits.
|
|
449
|
+
def record_cross_file_method(class_name, method_name, node, singleton: false)
|
|
450
|
+
symbol = "#{class_name}#{singleton ? '.' : '#'}#{method_name}"
|
|
392
451
|
if node
|
|
393
452
|
# ADR-46 slice 4 — pass the symbol so the recorder tracks this as a method-call (symbol-granularity) edge
|
|
394
453
|
# rather than a file-level edge.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
"#{class_name}##{method_name}"
|
|
398
|
-
)
|
|
454
|
+
source_table = singleton ? @discovery.discovered_singleton_def_sources : @discovery.discovered_def_sources
|
|
455
|
+
Analysis::DependencyRecorder.read_site(source_table.dig(class_name.to_s, method_name.to_sym), symbol)
|
|
399
456
|
else
|
|
400
|
-
Analysis::DependencyRecorder.read_missing(:method,
|
|
457
|
+
Analysis::DependencyRecorder.read_missing(:method, symbol)
|
|
401
458
|
end
|
|
402
459
|
end
|
|
403
460
|
private :record_cross_file_method
|
|
@@ -408,9 +465,9 @@ module Rigor
|
|
|
408
465
|
# implementation detail and go through this accessor.
|
|
409
466
|
def top_level_def_for(method_name)
|
|
410
467
|
table = @discovery.discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY]
|
|
411
|
-
|
|
412
|
-
record_cross_file_toplevel(method_name,
|
|
413
|
-
|
|
468
|
+
entry = table && table[method_name.to_sym] # live node or DefHandle (ADR-85 WD3)
|
|
469
|
+
record_cross_file_toplevel(method_name, entry) if Analysis::DependencyRecorder.active?
|
|
470
|
+
Inference::DefNodeResolver.resolve(entry)
|
|
414
471
|
end
|
|
415
472
|
|
|
416
473
|
# ADR-46 slice 3 — a top-level (`def helper` outside any class) call has NO class ancestry to walk, so unlike
|
|
@@ -440,9 +497,16 @@ module Rigor
|
|
|
440
497
|
# can point at `pre_eval:` (ADR-17) instead of reading as a bare unresolved call.
|
|
441
498
|
def user_def_site_for(class_name, method_name)
|
|
442
499
|
table = @discovery.discovered_def_sources[class_name.to_s]
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
500
|
+
site = table && table[method_name.to_sym]
|
|
501
|
+
# ADR-88 WD3 — record the SAME instance-side cross-file method edge {#user_def_for} records at :378, so a
|
|
502
|
+
# move / body-edit of `class_name#method_name`'s definition re-checks the consumer that named the
|
|
503
|
+
# defining file. `CheckRules#undefined_method_diagnostic` reads this to set `project_definition_site`
|
|
504
|
+
# (`"path:line"`) on a `call.undefined-method` for a project monkey-patch; without the edge, a line-shift
|
|
505
|
+
# in the defining file left the cached diagnostic pointing at a stale line (the ADR-46 symbol-granularity
|
|
506
|
+
# closure never re-checked the caller). Recording keys on the source-entry PRESENCE (truthy `site`),
|
|
507
|
+
# sound whether or not the caller also went through {#user_def_for}.
|
|
508
|
+
record_cross_file_method(class_name, method_name, site) if Analysis::DependencyRecorder.active?
|
|
509
|
+
site
|
|
446
510
|
end
|
|
447
511
|
|
|
448
512
|
# ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN
|
|
@@ -592,6 +656,14 @@ module Rigor
|
|
|
592
656
|
Inference::ExpressionTyper.new(scope: self, tracer: tracer).type_of(node)
|
|
593
657
|
end
|
|
594
658
|
|
|
659
|
+
# ADR-89 WD2 — the inferred return type of `def_node` called with `receiver` / `arg_types`, computed
|
|
660
|
+
# against THIS scope's discovery index (so cross-file dispatches in the body resolve). The incremental
|
|
661
|
+
# session re-drives a declaration-stable changed callee at each previously-observed call key to prove its
|
|
662
|
+
# return is unchanged before skipping its symbol dependents.
|
|
663
|
+
def user_method_return(def_node, receiver, arg_types)
|
|
664
|
+
Inference::ExpressionTyper.new(scope: self).return_type_for(def_node, receiver, arg_types)
|
|
665
|
+
end
|
|
666
|
+
|
|
595
667
|
# Statement-level evaluation: returns the pair `[type, scope']` where `type` is what the node produces and
|
|
596
668
|
# `scope'` is the scope observable after the node has run. The receiver scope is never mutated. See
|
|
597
669
|
# {Rigor::Inference::StatementEvaluator} for the catalogue of nodes that thread scope; everything else defers
|
|
@@ -651,7 +723,8 @@ module Rigor
|
|
|
651
723
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
652
724
|
dynamic_origins: @dynamic_origins,
|
|
653
725
|
local_origins: @local_origins,
|
|
654
|
-
ivar_origins: @ivar_origins
|
|
726
|
+
ivar_origins: @ivar_origins,
|
|
727
|
+
void_origins: @void_origins
|
|
655
728
|
)
|
|
656
729
|
self.class.new(
|
|
657
730
|
environment: environment, locals: locals,
|
|
@@ -665,7 +738,8 @@ module Rigor
|
|
|
665
738
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
666
739
|
dynamic_origins: dynamic_origins,
|
|
667
740
|
local_origins: local_origins,
|
|
668
|
-
ivar_origins: ivar_origins
|
|
741
|
+
ivar_origins: ivar_origins,
|
|
742
|
+
void_origins: void_origins
|
|
669
743
|
)
|
|
670
744
|
end
|
|
671
745
|
|
|
@@ -702,7 +776,8 @@ module Rigor
|
|
|
702
776
|
source_path: source_path,
|
|
703
777
|
dynamic_origins: @dynamic_origins,
|
|
704
778
|
local_origins: join_origins(@local_origins, other.local_origins),
|
|
705
|
-
ivar_origins: join_origins(@ivar_origins, other.ivar_origins)
|
|
779
|
+
ivar_origins: join_origins(@ivar_origins, other.ivar_origins),
|
|
780
|
+
void_origins: @void_origins
|
|
706
781
|
)
|
|
707
782
|
end
|
|
708
783
|
|
|
@@ -724,11 +799,30 @@ module Rigor
|
|
|
724
799
|
origins.key?(key) ? origins.reject { |k, _| k == key }.freeze : origins
|
|
725
800
|
end
|
|
726
801
|
|
|
802
|
+
# Two kinds of mark share this Set with opposite join semantics:
|
|
803
|
+
#
|
|
804
|
+
# - ADR-58 WD1 declaration-sourced optionality (`:ivar` / `:local`) joins by **intersection** — a nil is
|
|
805
|
+
# assumed a cross-method invariant only when BOTH branches agree its optionality is declaration-sourced.
|
|
806
|
+
# - ADR-67 WD6b inferred-parameter taint (`:inferred_param`) joins by **union** — a local is a lower-bound
|
|
807
|
+
# value on the branch where it derives from an inferred parameter, so if EITHER branch taints it, a
|
|
808
|
+
# downstream use could observe the lower-bound value and firing on it is an FP (`x = param else x = 5;
|
|
809
|
+
# x.foo`). Intersecting would lose the taint at the merge and re-surface the false positive.
|
|
727
810
|
def join_declaration_sourced(other)
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
811
|
+
mine = @declaration_sourced
|
|
812
|
+
theirs = other.declaration_sourced
|
|
813
|
+
return mine if mine.equal?(theirs)
|
|
814
|
+
|
|
815
|
+
inferred = Set.new
|
|
816
|
+
mine.each { |ref| inferred << ref if ref[0] == :inferred_param }
|
|
817
|
+
theirs.each { |ref| inferred << ref if ref[0] == :inferred_param }
|
|
818
|
+
intersected =
|
|
819
|
+
if mine.empty? || theirs.empty?
|
|
820
|
+
EMPTY_DECLARATION_SOURCED
|
|
821
|
+
else
|
|
822
|
+
mine.select { |ref| ref[0] != :inferred_param && theirs.include?(ref) }
|
|
823
|
+
end
|
|
824
|
+
merged = inferred.merge(intersected)
|
|
825
|
+
merged.empty? ? EMPTY_DECLARATION_SOURCED : merged.freeze
|
|
732
826
|
end
|
|
733
827
|
|
|
734
828
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
@@ -25,7 +25,11 @@ module Rigor
|
|
|
25
25
|
SKIP_DIAGNOSTIC_IDS = {
|
|
26
26
|
complex_shape: "sig.skipped.complex-shape",
|
|
27
27
|
user_authored: "sig.skipped.user-authored",
|
|
28
|
-
untyped_return: "sig.skipped.untyped-return"
|
|
28
|
+
untyped_return: "sig.skipped.untyped-return",
|
|
29
|
+
# The generator rendered a line `rbs` itself rejects. Skipping the method keeps sig-gen useful on a
|
|
30
|
+
# project with one pathological def, where failing the command outright would deny the user every other
|
|
31
|
+
# signature; the count is reported so the defect is not silent. See {SigGen::RbsValidity}.
|
|
32
|
+
unrenderable_rbs: "sig.skipped.unrenderable-rbs"
|
|
29
33
|
}.freeze
|
|
30
34
|
end
|
|
31
35
|
end
|
|
@@ -8,9 +8,11 @@ require_relative "../scope"
|
|
|
8
8
|
require_relative "../reflection"
|
|
9
9
|
require_relative "../type"
|
|
10
10
|
require_relative "../source/literals"
|
|
11
|
+
require_relative "../source/node_children"
|
|
11
12
|
require_relative "../inference/def_return_typer"
|
|
12
13
|
require_relative "../inference/scope_indexer"
|
|
13
14
|
require_relative "../inference/rbs_type_translator"
|
|
15
|
+
require_relative "rbs_validity"
|
|
14
16
|
|
|
15
17
|
module Rigor
|
|
16
18
|
module SigGen
|
|
@@ -33,6 +35,13 @@ module Rigor
|
|
|
33
35
|
# strictly accepts the inferred one (acceptance check under the engine's current `:gradual` mode; ADR-14
|
|
34
36
|
# reserves the eventual `:strict` mode).
|
|
35
37
|
class Generator # rubocop:disable Metrics/ClassLength
|
|
38
|
+
# Methods the generator rendered into RBS that `rbs` itself rejects. Populated by {#build_candidate}; each
|
|
39
|
+
# one is a Rigor rendering DEFECT, not a property of the user's code, so the CLI reports them as such.
|
|
40
|
+
UnrenderableMethod = Data.define(:path, :class_name, :method_name, :rbs, :error)
|
|
41
|
+
|
|
42
|
+
# @return [Array<UnrenderableMethod>] empty on a healthy run; read after {#run}.
|
|
43
|
+
attr_reader :unrenderable
|
|
44
|
+
|
|
36
45
|
# @param configuration [Rigor::Configuration]
|
|
37
46
|
# @param paths [Array<String>] files / directories to scan.
|
|
38
47
|
# @param observations [Hash{[String, Symbol] => Array<Array<Rigor::Type>>}]
|
|
@@ -52,6 +61,8 @@ module Rigor
|
|
|
52
61
|
@module_function_methods = Set.new
|
|
53
62
|
@class_shells = Set.new
|
|
54
63
|
@class_superclasses = {}
|
|
64
|
+
# Whole-run, NOT per-file: a rendering defect is reported once at the end of the run.
|
|
65
|
+
@unrenderable = []
|
|
55
66
|
end
|
|
56
67
|
|
|
57
68
|
# Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
|
|
@@ -159,7 +170,7 @@ module Rigor
|
|
|
159
170
|
return
|
|
160
171
|
end
|
|
161
172
|
|
|
162
|
-
node.
|
|
173
|
+
node.rigor_each_child do |child|
|
|
163
174
|
walk_defs(child, prefix, in_singleton_class, module_function_active, out)
|
|
164
175
|
end
|
|
165
176
|
end
|
|
@@ -264,12 +275,33 @@ module Rigor
|
|
|
264
275
|
# Wraps `MethodCandidate.new` so every candidate carries the per-file `@namespace_kinds` map AND the
|
|
265
276
|
# `@class_shells` set — the Writer's nested-syntax emission consults both to pick `module` vs `class` for
|
|
266
277
|
# each segment and to emit empty `Const = Data.define(...)` declarations.
|
|
267
|
-
|
|
278
|
+
#
|
|
279
|
+
# It is also where every rendered line is PARSED before it can leave the generator (see
|
|
280
|
+
# {SigGen::RbsValidity}). This is the one construction point all candidates pass through, so guarding it
|
|
281
|
+
# covers every mode — `--print`, `--diff`, `--write`, and the MCP surface — rather than each emit site
|
|
282
|
+
# separately. A line `rbs` rejects demotes the candidate to `:skipped` instead of being emitted: a
|
|
283
|
+
# signature we cannot parse is worse than no signature, because it takes the whole FILE down with it
|
|
284
|
+
# (the consumer quarantines it, so every other type in that file vanishes too).
|
|
285
|
+
def build_candidate(**fields)
|
|
286
|
+
fields = demote_unrenderable(fields)
|
|
268
287
|
MethodCandidate.new(
|
|
269
288
|
namespace_kinds: @namespace_kinds,
|
|
270
289
|
class_shells: @class_shells.to_a,
|
|
271
290
|
class_superclasses: @class_superclasses,
|
|
272
|
-
**
|
|
291
|
+
**fields
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def demote_unrenderable(fields)
|
|
296
|
+
error = RbsValidity.method_line_error(fields[:rbs])
|
|
297
|
+
return fields if error.nil?
|
|
298
|
+
|
|
299
|
+
@unrenderable << UnrenderableMethod.new(
|
|
300
|
+
path: fields[:path], class_name: fields[:class_name],
|
|
301
|
+
method_name: fields[:method_name], rbs: fields[:rbs], error: error
|
|
302
|
+
)
|
|
303
|
+
fields.merge(
|
|
304
|
+
classification: Classification::SKIPPED, skip_reason: :unrenderable_rbs, rbs: nil
|
|
273
305
|
)
|
|
274
306
|
end
|
|
275
307
|
|
|
@@ -796,7 +828,7 @@ module Rigor
|
|
|
796
828
|
collect_attr_call(node, prefix, in_singleton_class, ctx)
|
|
797
829
|
end
|
|
798
830
|
|
|
799
|
-
node.
|
|
831
|
+
node.rigor_each_child { |child| walk_attr_calls(child, prefix, in_singleton_class, ctx) }
|
|
800
832
|
end
|
|
801
833
|
|
|
802
834
|
def collect_attr_call(call_node, prefix, in_singleton_class, ctx)
|
|
@@ -872,7 +904,7 @@ module Rigor
|
|
|
872
904
|
return
|
|
873
905
|
end
|
|
874
906
|
|
|
875
|
-
node.
|
|
907
|
+
node.rigor_each_child { |c| collect_init_ivar_obs(c, prefix, result) }
|
|
876
908
|
end
|
|
877
909
|
|
|
878
910
|
# Derive { attr_name_sym => Type } for a single `def initialize` by matching `@ivar = param_name`
|
|
@@ -950,7 +982,7 @@ module Rigor
|
|
|
950
982
|
node.is_a?(Prism::ClassNode) ||
|
|
951
983
|
node.is_a?(Prism::ModuleNode)
|
|
952
984
|
|
|
953
|
-
node.
|
|
985
|
+
node.rigor_each_child { |c| scan_ivar_param_assignments(c, param_names, result) }
|
|
954
986
|
end
|
|
955
987
|
|
|
956
988
|
def build_attr_candidates(call_name, class_name, attr_name, ivar_type, ctx)
|