rigortype 0.1.15 → 0.1.17
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 +4 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
- data/lib/rigor/analysis/check_rules.rb +174 -71
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +58 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner.rb +485 -29
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +56 -51
- data/lib/rigor/cache/incremental_snapshot.rb +147 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +99 -1
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +52 -10
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +260 -48
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +13 -3
- data/lib/rigor/environment/rbs_loader.rb +335 -4
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +149 -22
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +100 -23
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +147 -60
- data/lib/rigor/inference/narrowing.rb +202 -5
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +257 -11
- data/lib/rigor/inference/statement_evaluator.rb +110 -26
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +337 -2
- data/lib/rigor/plugin/box.rb +64 -0
- data/lib/rigor/plugin/inflector.rb +121 -0
- data/lib/rigor/plugin/isolation.rb +191 -0
- data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
- data/lib/rigor/plugin/macro.rb +1 -0
- data/lib/rigor/plugin/manifest.rb +120 -23
- data/lib/rigor/plugin/node_context.rb +62 -0
- data/lib/rigor/plugin/registry.rb +49 -1
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope.rb +123 -9
- data/lib/rigor/sig_gen/generator.rb +2 -3
- data/lib/rigor/sig_gen/observation_collector.rb +2 -2
- data/lib/rigor/source/literals.rb +118 -0
- data/lib/rigor/source/node_walker.rb +26 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +23 -1
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +69 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
- data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +48 -33
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/inference.rbs +22 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -1
- data/sig/rigor/plugin/base.rbs +58 -3
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/plugin/manifest.rbs +31 -1
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +5 -0
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +11 -1
- data/skills/rigor-plugin-author/SKILL.md +13 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- metadata +73 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
|
@@ -30,6 +30,24 @@ module Rigor
|
|
|
30
30
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
31
31
|
# rubocop:disable Metrics/ClassLength
|
|
32
32
|
class RbsLoader
|
|
33
|
+
# Buffer name stamped on the `module` declarations synthesized by
|
|
34
|
+
# {.synthesize_missing_namespaces}. Re-read off the built env by
|
|
35
|
+
# {#synthesized_namespaces} so the analysis layer can surface an
|
|
36
|
+
# `:info` diagnostic naming the project's malformed-RBS namespaces
|
|
37
|
+
# — robust across the marshalled env cache, since the sentinel
|
|
38
|
+
# rides along on each synthetic declaration's location.
|
|
39
|
+
SYNTHETIC_NAMESPACE_BUFFER = "(rigor: synthesized namespaces)"
|
|
40
|
+
|
|
41
|
+
# Buffer name stamped on the stub `class` / `module` declarations
|
|
42
|
+
# synthesized by {.stub_missing_referenced_types} for types the
|
|
43
|
+
# project's RBS references but no loaded signature declares.
|
|
44
|
+
# {#synthesized_stub_types} reads them back off the built env (so
|
|
45
|
+
# the answer survives the marshalled env cache), and
|
|
46
|
+
# {#synthesized_type_names} folds them together with the
|
|
47
|
+
# namespace stubs into the set {MethodDispatcher} resolves to
|
|
48
|
+
# `Dynamic[Top]` (no false `call.undefined-method`).
|
|
49
|
+
SYNTHETIC_STUB_BUFFER = "(rigor: synthesized stub types)"
|
|
50
|
+
|
|
33
51
|
class << self
|
|
34
52
|
def default
|
|
35
53
|
@default ||= new.freeze
|
|
@@ -72,7 +90,172 @@ module Rigor
|
|
|
72
90
|
end
|
|
73
91
|
env = RBS::Environment.from_loader(rbs_loader)
|
|
74
92
|
add_virtual_rbs(env, virtual_rbs)
|
|
75
|
-
env
|
|
93
|
+
synthesize_missing_namespaces(env)
|
|
94
|
+
resolved = env.resolve_type_names
|
|
95
|
+
stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# ADR-5 robustness, second tier. A project `signature_paths:`
|
|
99
|
+
# RBS that *references* a type no loaded signature declares —
|
|
100
|
+
# `def x: () -> DRb::DRbServer` when the `drb` RBS is not
|
|
101
|
+
# available, or a stale reference to its own removed
|
|
102
|
+
# `Textbringer::EditorError` — makes
|
|
103
|
+
# `RBS::DefinitionBuilder#build_instance` raise
|
|
104
|
+
# `NoTypeFoundError`, and (per RBS's all-or-nothing per-class
|
|
105
|
+
# build) that single unresolved reference takes down EVERY
|
|
106
|
+
# method on the class, not just the one signature. Observed on
|
|
107
|
+
# shugo/textbringer: one `DRb::DRbServer` reference left the
|
|
108
|
+
# whole `Textbringer::Commands` module — including its
|
|
109
|
+
# 186-call-site `define_command` DSL — resolving as
|
|
110
|
+
# `Dynamic[Top]`.
|
|
111
|
+
#
|
|
112
|
+
# We synthesize an empty stub for each such referenced-but-
|
|
113
|
+
# undeclared type so the rest of the class builds. A leaf type
|
|
114
|
+
# is stubbed as `class`, its enclosing namespaces as `module`.
|
|
115
|
+
# Stubbed types carry no methods, so a call against a value of
|
|
116
|
+
# a stubbed type would otherwise mis-fire `call.undefined-method`;
|
|
117
|
+
# {MethodDispatcher} consults {#synthesized_type_names} and
|
|
118
|
+
# resolves such calls to `Dynamic[Top]` instead (the same
|
|
119
|
+
# no-false-positive contract as the dependency-source tier).
|
|
120
|
+
#
|
|
121
|
+
# Detection re-uses RBS's own builder (correct by construction):
|
|
122
|
+
# build every PROJECT class and read the missing name out of the
|
|
123
|
+
# raised error. Bounded to `signature_paths` classes (stdlib /
|
|
124
|
+
# vendored RBS is well-formed) and to {MAX_STUB_PASSES}
|
|
125
|
+
# iterations — a fresh stub can expose a deeper reference the
|
|
126
|
+
# first build error hid, but empty stubs reference nothing, so
|
|
127
|
+
# the fixpoint converges quickly.
|
|
128
|
+
MAX_STUB_PASSES = 5
|
|
129
|
+
|
|
130
|
+
def stub_missing_referenced_types(base_env, resolved, project_files)
|
|
131
|
+
return resolved if project_files.empty?
|
|
132
|
+
|
|
133
|
+
MAX_STUB_PASSES.times do
|
|
134
|
+
missing = unresolved_referenced_types(resolved, project_files)
|
|
135
|
+
break if missing.empty?
|
|
136
|
+
|
|
137
|
+
append_stub_declarations(base_env, missing)
|
|
138
|
+
resolved = base_env.resolve_type_names
|
|
139
|
+
end
|
|
140
|
+
resolved
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Robustness (ADR-5): a project whose RBS declares qualified
|
|
144
|
+
# names (`class Foo::Bar`) without ever declaring the enclosing
|
|
145
|
+
# namespace (`module Foo`) is invalid by upstream RBS rules —
|
|
146
|
+
# `RBS::DefinitionBuilder#build_instance` raises
|
|
147
|
+
# `NoTypeFoundError: Could not find ::Foo`, which the loader's
|
|
148
|
+
# fail-soft rescue turns into a silent dispatch miss (every
|
|
149
|
+
# method on every such class degrades to `Dynamic[Top]`). This
|
|
150
|
+
# is a common authoring mistake (e.g. shugo/textbringer ships a
|
|
151
|
+
# `sig/` that `rbs validate` itself rejects). Rather than let an
|
|
152
|
+
# otherwise-usable signature set contribute nothing, synthesize
|
|
153
|
+
# an empty `module` declaration for each undeclared enclosing
|
|
154
|
+
# namespace so the definitions build. We only ever add names
|
|
155
|
+
# that are absent — a genuinely-declared namespace (module or
|
|
156
|
+
# class, here or in a loaded gem) is left untouched.
|
|
157
|
+
def synthesize_missing_namespaces(env)
|
|
158
|
+
missing = collect_missing_namespaces(env)
|
|
159
|
+
return if missing.empty?
|
|
160
|
+
|
|
161
|
+
source = missing.map { |name| "module #{name}\nend\n" }.join
|
|
162
|
+
buffer = ::RBS::Buffer.new(name: SYNTHETIC_NAMESPACE_BUFFER, content: source)
|
|
163
|
+
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
164
|
+
env.add_source(::RBS::Source::RBS.new(buffer, directives || [], decls || []))
|
|
165
|
+
rescue ::RBS::BaseError
|
|
166
|
+
# Fail-soft: synthesis is an opportunistic uplift, never a
|
|
167
|
+
# hard requirement. A parse failure here just leaves the env
|
|
168
|
+
# as it was (dispatch misses on the affected classes).
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Returns the `::`-stripped names of every enclosing namespace
|
|
173
|
+
# that some declaration references but no declaration defines,
|
|
174
|
+
# shallowest-first so the synthesized source declares `Foo`
|
|
175
|
+
# before `Foo::Bar`.
|
|
176
|
+
def collect_missing_namespaces(env)
|
|
177
|
+
declared = env.class_decls.keys.to_set
|
|
178
|
+
missing = {}
|
|
179
|
+
env.class_decls.each_key do |type_name|
|
|
180
|
+
path = type_name.namespace.path
|
|
181
|
+
path.each_index do |i|
|
|
182
|
+
prefix = path[0..i]
|
|
183
|
+
full = ::RBS::TypeName.parse("::#{prefix.join('::')}")
|
|
184
|
+
missing[prefix.join("::")] = prefix.length unless declared.include?(full)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
missing.sort_by { |_name, depth| depth }.map(&:first)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The absolute paths of every `.rbs` file under the project's
|
|
191
|
+
# `signature_paths:` (NOT vendored / stdlib RBS — those are
|
|
192
|
+
# well-formed, so attempting to build them would only waste
|
|
193
|
+
# time). Used to scope the referenced-type build sweep.
|
|
194
|
+
def project_sig_files(signature_paths)
|
|
195
|
+
signature_paths.flat_map do |path|
|
|
196
|
+
path = Pathname(path) unless path.is_a?(Pathname)
|
|
197
|
+
next [] unless path.directory?
|
|
198
|
+
|
|
199
|
+
Dir.glob(path.join("**", "*.rbs")).map { |p| File.expand_path(p) }
|
|
200
|
+
end.to_set
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Builds every project class (instance + singleton side) and
|
|
204
|
+
# returns the `::`-stripped names of the types whose absence
|
|
205
|
+
# raised `NoTypeFoundError`. Only the FIRST missing reference
|
|
206
|
+
# per class surfaces per build, which is why the caller loops.
|
|
207
|
+
def unresolved_referenced_types(env, project_files)
|
|
208
|
+
builder = ::RBS::DefinitionBuilder.new(env: env)
|
|
209
|
+
missing = []
|
|
210
|
+
env.class_decls.each do |type_name, entry|
|
|
211
|
+
next unless project_entry?(entry, project_files)
|
|
212
|
+
|
|
213
|
+
%i[build_instance build_singleton].each do |build|
|
|
214
|
+
builder.public_send(build, type_name)
|
|
215
|
+
rescue ::RBS::NoTypeFoundError => e
|
|
216
|
+
name = e.message[/Could not find (\S+)/, 1]
|
|
217
|
+
missing << name.sub(/\A::/, "") if name
|
|
218
|
+
rescue ::RBS::BaseError
|
|
219
|
+
# Other build failures (duplicate decl, mixin cycle, ...)
|
|
220
|
+
# are not ours to repair here — leave them fail-soft.
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
missing.uniq
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# True when a `class_decls` entry was declared in one of the
|
|
227
|
+
# project's own signature files (by declaration location), so
|
|
228
|
+
# the sweep skips the bundled stdlib / vendored universe.
|
|
229
|
+
def project_entry?(entry, project_files)
|
|
230
|
+
decl = entry.respond_to?(:primary_decl) ? entry.primary_decl : nil
|
|
231
|
+
location = decl&.location
|
|
232
|
+
buffer_name = location&.buffer&.name
|
|
233
|
+
return false unless buffer_name
|
|
234
|
+
|
|
235
|
+
project_files.include?(File.expand_path(buffer_name.to_s))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Adds empty stub declarations for the missing referenced types
|
|
239
|
+
# (and any enclosing namespace they need) to the pre-resolve
|
|
240
|
+
# env, tagged with {SYNTHETIC_STUB_BUFFER}. A name that is a
|
|
241
|
+
# prefix of another name is declared `module` (it is a
|
|
242
|
+
# namespace); a leaf is declared `class` (referenced types
|
|
243
|
+
# appear in instance position far more often than as mixins).
|
|
244
|
+
def append_stub_declarations(base_env, missing)
|
|
245
|
+
names = missing.to_set
|
|
246
|
+
missing.each do |name|
|
|
247
|
+
parts = name.split("::")
|
|
248
|
+
(1...parts.length).each { |i| names << parts[0, i].join("::") }
|
|
249
|
+
end
|
|
250
|
+
source = names.sort_by { |n| n.count(":") }.map do |name|
|
|
251
|
+
keyword = names.any? { |other| other != name && other.start_with?("#{name}::") } ? "module" : "class"
|
|
252
|
+
"#{keyword} #{name}\nend\n"
|
|
253
|
+
end.join
|
|
254
|
+
buffer = ::RBS::Buffer.new(name: SYNTHETIC_STUB_BUFFER, content: source)
|
|
255
|
+
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
256
|
+
base_env.add_source(::RBS::Source::RBS.new(buffer, directives || [], decls || []))
|
|
257
|
+
rescue ::RBS::BaseError
|
|
258
|
+
nil
|
|
76
259
|
end
|
|
77
260
|
|
|
78
261
|
# ADR-32 WD4 — merge synthesised-from-source RBS strings
|
|
@@ -192,6 +375,40 @@ module Rigor
|
|
|
192
375
|
@hierarchy = RbsHierarchy.new(self)
|
|
193
376
|
end
|
|
194
377
|
|
|
378
|
+
# The enclosing namespaces {.synthesize_missing_namespaces} had to
|
|
379
|
+
# invent because the project's `signature_paths:` RBS declared
|
|
380
|
+
# qualified names (`class Foo::Bar`) without ever declaring `Foo`.
|
|
381
|
+
# Recovered by scanning the built env for class/module entries
|
|
382
|
+
# whose every declaration originated from the synthetic buffer, so
|
|
383
|
+
# the answer survives the marshalled-env cache (where no build-time
|
|
384
|
+
# collector would). Returns `::`-stripped names, shallowest-first.
|
|
385
|
+
# Empty for a well-formed sig set (the common case) and whenever
|
|
386
|
+
# the env failed to build.
|
|
387
|
+
def synthesized_namespaces
|
|
388
|
+
names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The referenced-but-undeclared types
|
|
392
|
+
# {.stub_missing_referenced_types} stubbed so the project classes
|
|
393
|
+
# that mention them could build (e.g. an unavailable
|
|
394
|
+
# `DRb::DRbServer`, or a stale `Textbringer::EditorError`).
|
|
395
|
+
# Recovered off the built env like {#synthesized_namespaces}, so
|
|
396
|
+
# it survives the marshalled-env cache.
|
|
397
|
+
def synthesized_stub_types
|
|
398
|
+
names_synthesized_in(SYNTHETIC_STUB_BUFFER)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Every type name Rigor invented to make an otherwise-inert /
|
|
402
|
+
# unbuildable project signature set resolve — both the namespace
|
|
403
|
+
# stubs and the referenced-type stubs. {MethodDispatcher} resolves
|
|
404
|
+
# a call whose receiver is one of these (and that no real
|
|
405
|
+
# signature answered) to `Dynamic[Top]`, so the empty stub never
|
|
406
|
+
# mis-fires `call.undefined-method`. Memoised; empty (and cheap)
|
|
407
|
+
# for the common well-formed sig set.
|
|
408
|
+
def synthesized_type_names
|
|
409
|
+
@state[:synthesized_type_names] ||= (synthesized_namespaces + synthesized_stub_types).to_set
|
|
410
|
+
end
|
|
411
|
+
|
|
195
412
|
# Returns true when an RBS class or module declaration with the given
|
|
196
413
|
# name is loaded. Accepts unprefixed or top-level-prefixed names
|
|
197
414
|
# ("Integer" or "::Integer"). Memoized per-name (positive and
|
|
@@ -284,6 +501,27 @@ module Rigor
|
|
|
284
501
|
# registry rather than crashing.
|
|
285
502
|
end
|
|
286
503
|
|
|
504
|
+
# Like {#each_class_decl_annotation}, but also yields the
|
|
505
|
+
# owning class / module's RBS name as the first block
|
|
506
|
+
# argument: `(class_name, annotation_string, location)`. Used
|
|
507
|
+
# by {Rigor::RbsExtended::ConformanceChecker} to resolve a
|
|
508
|
+
# `rigor:v1:conforms-to` directive back to the class it
|
|
509
|
+
# annotates. Same fail-soft policy as the un-named variant.
|
|
510
|
+
def each_class_decl_annotation_with_name
|
|
511
|
+
return enum_for(:each_class_decl_annotation_with_name) unless block_given?
|
|
512
|
+
return if env.nil?
|
|
513
|
+
|
|
514
|
+
env.class_decls.each do |rbs_name, entry|
|
|
515
|
+
entry.each_decl do |decl|
|
|
516
|
+
next unless decl.respond_to?(:annotations)
|
|
517
|
+
|
|
518
|
+
decl.annotations.each { |a| yield rbs_name.to_s, a.string, a.location }
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
rescue ::RBS::BaseError, ::Ractor::IsolationError
|
|
522
|
+
# fail-soft: see #each_class_decl_annotation.
|
|
523
|
+
end
|
|
524
|
+
|
|
287
525
|
# Returns a frozen `Hash<String, String>` mapping each loaded
|
|
288
526
|
# class / module name (top-level prefixed) to the file path of
|
|
289
527
|
# its FIRST declaration's RBS source. Used by
|
|
@@ -354,6 +592,44 @@ module Rigor
|
|
|
354
592
|
definition.methods[method_name.to_sym]
|
|
355
593
|
end
|
|
356
594
|
|
|
595
|
+
# @return [Array<Symbol>, nil] every instance-method name on
|
|
596
|
+
# `class_name` — own, inherited, and included — as resolved
|
|
597
|
+
# by `RBS::DefinitionBuilder`. Returns `nil` (NOT `[]`) when
|
|
598
|
+
# the class definition cannot be built so callers can tell
|
|
599
|
+
# "no methods" apart from "unknown class". Used by the
|
|
600
|
+
# `rigor:v1:conforms-to` presence check
|
|
601
|
+
# ({Rigor::RbsExtended::ConformanceChecker}).
|
|
602
|
+
def instance_method_names(class_name)
|
|
603
|
+
definition = instance_definition(class_name)
|
|
604
|
+
return nil unless definition
|
|
605
|
+
|
|
606
|
+
definition.methods.keys
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# @return [RBS::Definition, nil] the built definition for the RBS
|
|
610
|
+
# interface `interface_name` (`_RewindableStream`), whose `.methods`
|
|
611
|
+
# are the required members (including interface-ancestor members).
|
|
612
|
+
# Returns `nil` when the name does not resolve to a loaded interface
|
|
613
|
+
# (a typo, or the defining library / sig set is not on the load
|
|
614
|
+
# path). Fail-soft on RBS build errors.
|
|
615
|
+
def interface_definition(interface_name)
|
|
616
|
+
rbs_name = parse_type_name(interface_name)
|
|
617
|
+
return nil unless rbs_name
|
|
618
|
+
return nil if env.nil?
|
|
619
|
+
return nil unless env.interface_decls.key?(rbs_name)
|
|
620
|
+
|
|
621
|
+
builder.build_interface(rbs_name)
|
|
622
|
+
rescue ::RBS::BaseError
|
|
623
|
+
nil
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# @return [Array<Symbol>, nil] every method name required by the RBS
|
|
627
|
+
# interface `interface_name`, or nil when it does not resolve. Thin
|
|
628
|
+
# accessor over {#interface_definition} for the presence check.
|
|
629
|
+
def interface_method_names(interface_name)
|
|
630
|
+
interface_definition(interface_name)&.methods&.keys
|
|
631
|
+
end
|
|
632
|
+
|
|
357
633
|
# @return [RBS::Definition, nil] the resolved singleton (class
|
|
358
634
|
# object) definition for `class_name`. The methods on this
|
|
359
635
|
# definition are the *class methods* of `class_name`, including
|
|
@@ -546,6 +822,47 @@ module Rigor
|
|
|
546
822
|
|
|
547
823
|
private
|
|
548
824
|
|
|
825
|
+
# The `::`-stripped names of every class/module entry whose
|
|
826
|
+
# declarations ALL originated from the given sentinel buffer —
|
|
827
|
+
# i.e. names Rigor synthesized, not names the project declared.
|
|
828
|
+
# Reads off the built env so the answer survives the marshalled
|
|
829
|
+
# env cache; shallowest-first. Empty when the env failed to build.
|
|
830
|
+
def names_synthesized_in(buffer_name)
|
|
831
|
+
e = env
|
|
832
|
+
return [] if e.nil?
|
|
833
|
+
|
|
834
|
+
names = e.class_decls.filter_map do |type_name, entry|
|
|
835
|
+
decls = entry_declarations(entry)
|
|
836
|
+
next if decls.empty?
|
|
837
|
+
next unless decls.all? { |decl| synthetic_decl?(decl, buffer_name) }
|
|
838
|
+
|
|
839
|
+
type_name.to_s.sub(/\A::/, "")
|
|
840
|
+
end
|
|
841
|
+
names.sort_by { |name| name.count("::") }
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
# Collects the AST declaration nodes behind a `class_decls`
|
|
845
|
+
# entry. RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl`;
|
|
846
|
+
# the older single-`decl` shape is handled defensively so the
|
|
847
|
+
# loader survives an rbs-gem minor bump.
|
|
848
|
+
def entry_declarations(entry)
|
|
849
|
+
if entry.respond_to?(:each_decl)
|
|
850
|
+
[].tap { |acc| entry.each_decl { |decl| acc << decl } }
|
|
851
|
+
elsif entry.respond_to?(:decl)
|
|
852
|
+
[entry.decl]
|
|
853
|
+
else
|
|
854
|
+
[]
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# True when an AST declaration was emitted into `buffer_name`
|
|
859
|
+
# (one of the synthetic-source sentinels) — identified by the
|
|
860
|
+
# buffer name on its location.
|
|
861
|
+
def synthetic_decl?(decl, buffer_name)
|
|
862
|
+
location = decl.respond_to?(:location) ? decl.location : nil
|
|
863
|
+
location&.buffer&.name.to_s == buffer_name
|
|
864
|
+
end
|
|
865
|
+
|
|
549
866
|
def constant_type_table
|
|
550
867
|
@constant_type_table ||= begin
|
|
551
868
|
require_relative "../cache/rbs_constant_table"
|
|
@@ -725,14 +1042,28 @@ module Rigor
|
|
|
725
1042
|
nil
|
|
726
1043
|
end
|
|
727
1044
|
|
|
1045
|
+
# Memoised on `@state` (the per-loader store also holding `:env` /
|
|
1046
|
+
# `:builder`): `RBS::TypeName.parse` is a pure, deterministic
|
|
1047
|
+
# function of the normalised string, and the `RBS::TypeName` it
|
|
1048
|
+
# returns is a frozen value object safe to share across callers
|
|
1049
|
+
# (every consumer only reads it — `env.class_decls.key?` /
|
|
1050
|
+
# `builder.build_*`). The same handful of class names are parsed
|
|
1051
|
+
# on nearly every call-site dispatch, so this was a top allocation
|
|
1052
|
+
# site; caching the immutable result (nil included) removes it.
|
|
728
1053
|
def parse_type_name(name)
|
|
729
1054
|
s = name.to_s
|
|
730
1055
|
return nil if s.empty?
|
|
731
1056
|
|
|
732
1057
|
s = "::#{s}" unless s.start_with?("::")
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1058
|
+
cache = (@state[:type_name_cache] ||= {})
|
|
1059
|
+
return cache[s] if cache.key?(s)
|
|
1060
|
+
|
|
1061
|
+
cache[s] =
|
|
1062
|
+
begin
|
|
1063
|
+
RBS::TypeName.parse(s)
|
|
1064
|
+
rescue ::RBS::BaseError
|
|
1065
|
+
nil
|
|
1066
|
+
end
|
|
736
1067
|
end
|
|
737
1068
|
|
|
738
1069
|
def compute_class_known(name)
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -512,8 +512,14 @@ module Rigor
|
|
|
512
512
|
# presence check without materialising a type carrier.
|
|
513
513
|
def class_known?(name)
|
|
514
514
|
return true if class_registry.nominal_for_name(name)
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
return true if class_known_in_rbs?(name)
|
|
516
|
+
|
|
517
|
+
# ADR-36 nested-class emission — a variant subclass the
|
|
518
|
+
# substrate synthesised (e.g. `Shape::Circle` from a
|
|
519
|
+
# `variants do variant Circle, Float end` block) is a real
|
|
520
|
+
# class for resolution purposes even though no RBS / source
|
|
521
|
+
# declares it.
|
|
522
|
+
@synthetic_method_index&.knows_class?(name) || false
|
|
517
523
|
end
|
|
518
524
|
|
|
519
525
|
# ADR-15 Phase 2b — returns the loader's read-only,
|
|
@@ -174,8 +174,7 @@ module Rigor
|
|
|
174
174
|
|
|
175
175
|
def bind_trailing_positionals(params_node, bindings, cursor)
|
|
176
176
|
params_node.posts.each do |param|
|
|
177
|
-
|
|
178
|
-
bindings[name] = positional_type_at(cursor) if name
|
|
177
|
+
bind_required_param(param, cursor, bindings)
|
|
179
178
|
cursor += 1
|
|
180
179
|
end
|
|
181
180
|
cursor
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# Opt-in counters for the hard-coded inference cutoffs — the
|
|
6
|
+
# "budget" guards that silently return `Dynamic[top]` / `nil` /
|
|
7
|
+
# a fallback bound rather than emitting a diagnostic. These are
|
|
8
|
+
# the *operative* cutoffs in the engine today (the configurable
|
|
9
|
+
# `budgets:` table in docs/type-specification/inference-budgets.md
|
|
10
|
+
# is not yet wired); counting how often each fires on a real
|
|
11
|
+
# project is the only way to see where inference actually stops.
|
|
12
|
+
#
|
|
13
|
+
# Three categories, one per guard site:
|
|
14
|
+
#
|
|
15
|
+
# - {RECURSION_GUARD} — `ExpressionTyper#infer_user_method_return`
|
|
16
|
+
# detected a `(receiver, method)` cycle and returned
|
|
17
|
+
# `Dynamic[top]` (the de-facto recursion-depth budget, effective
|
|
18
|
+
# depth 1).
|
|
19
|
+
# - {ANCESTOR_WALK_LIMIT} — `resolve_user_def_through_ancestors`
|
|
20
|
+
# hit the 100-node BFS cap and gave up resolving the self-call.
|
|
21
|
+
# - {HKT_FUEL_EXHAUSTED} — `HktReducer` ran out of its reduction
|
|
22
|
+
# fuel budget and unwound to `app.bound`.
|
|
23
|
+
#
|
|
24
|
+
# Enabled only when `RIGOR_BUDGET_TRACE` is set (to any non-empty
|
|
25
|
+
# value) in the environment, or via {enable!} in tests. When
|
|
26
|
+
# disabled, {hit} is a single boolean check and returns
|
|
27
|
+
# immediately, so normal runs pay nothing.
|
|
28
|
+
#
|
|
29
|
+
# Counters are process-global (Mutex-guarded) so they aggregate
|
|
30
|
+
# across threads, but they do NOT cross `fork` boundaries — run
|
|
31
|
+
# `rigor check --workers 0` to keep all inference in one process
|
|
32
|
+
# when collecting a trace.
|
|
33
|
+
module BudgetTrace
|
|
34
|
+
RECURSION_GUARD = :recursion_guard
|
|
35
|
+
ANCESTOR_WALK_LIMIT = :ancestor_walk_limit
|
|
36
|
+
HKT_FUEL_EXHAUSTED = :hkt_fuel_exhausted
|
|
37
|
+
|
|
38
|
+
CATEGORIES = [RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED].freeze
|
|
39
|
+
|
|
40
|
+
# Distribution (histogram) categories — read-only observations of
|
|
41
|
+
# a value's size at a site, used to choose budget defaults from an
|
|
42
|
+
# observed tail rather than a guess (ADR-41 WD3 / Slice 2a). No cap
|
|
43
|
+
# is enforced; these only record. `UNION_ARITY` is the member count
|
|
44
|
+
# of every `Type::Union` that `Combinator.union` produces — the
|
|
45
|
+
# distribution the `union_size` budget default should be set from.
|
|
46
|
+
UNION_ARITY = :union_arity
|
|
47
|
+
|
|
48
|
+
DISTRIBUTION_CATEGORIES = [UNION_ARITY].freeze
|
|
49
|
+
|
|
50
|
+
@enabled = !ENV["RIGOR_BUDGET_TRACE"].to_s.empty?
|
|
51
|
+
@mutex = Mutex.new
|
|
52
|
+
@counts = Hash.new(0)
|
|
53
|
+
@distributions = Hash.new { |h, k| h[k] = Hash.new(0) }
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
def enabled?
|
|
58
|
+
@enabled
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Test / programmatic toggles. Production enablement is the
|
|
62
|
+
# `RIGOR_BUDGET_TRACE` env var read once at load time.
|
|
63
|
+
def enable!
|
|
64
|
+
@enabled = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def disable!
|
|
68
|
+
@enabled = false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Records one firing of `category`. No-op (one boolean check)
|
|
72
|
+
# when tracing is disabled.
|
|
73
|
+
def hit(category)
|
|
74
|
+
return unless @enabled
|
|
75
|
+
|
|
76
|
+
@mutex.synchronize { @counts[category] += 1 }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Frozen snapshot of the current counts, every known category
|
|
80
|
+
# present (zero-filled) so consumers can render a stable table.
|
|
81
|
+
def snapshot
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
CATEGORIES.to_h { |category| [category, @counts[category]] }.freeze
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Records one observation of `value` (an Integer size) into
|
|
88
|
+
# `category`'s histogram. No-op (one boolean check) when disabled.
|
|
89
|
+
def observe(category, value)
|
|
90
|
+
return unless @enabled
|
|
91
|
+
|
|
92
|
+
@mutex.synchronize { @distributions[category][value] += 1 }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Frozen `{value => count}` histogram for a distribution category.
|
|
96
|
+
def distribution(category)
|
|
97
|
+
@mutex.synchronize { @distributions[category].dup.freeze }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Summary of a distribution category: total observation count, max
|
|
101
|
+
# observed value, selected percentiles, and how many observations
|
|
102
|
+
# met or exceeded each threshold in `over`. Percentiles use the
|
|
103
|
+
# nearest-rank method over the expanded sample.
|
|
104
|
+
def summarize(category, over: [])
|
|
105
|
+
hist = distribution(category)
|
|
106
|
+
total = hist.values.sum
|
|
107
|
+
return { count: 0, max: 0, percentiles: {}, over: over.to_h { |t| [t, 0] } } if total.zero?
|
|
108
|
+
|
|
109
|
+
sorted = hist.keys.sort
|
|
110
|
+
{ count: total,
|
|
111
|
+
max: sorted.last,
|
|
112
|
+
percentiles: { p50: percentile(hist, total, 0.50), p90: percentile(hist, total, 0.90),
|
|
113
|
+
p99: percentile(hist, total, 0.99) },
|
|
114
|
+
over: over.to_h { |t| [t, hist.sum { |value, n| value >= t ? n : 0 }] } }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Nearest-rank percentile over a `{value => count}` histogram
|
|
118
|
+
# without materialising the full sample.
|
|
119
|
+
def percentile(hist, total, fraction)
|
|
120
|
+
rank = (fraction * total).ceil
|
|
121
|
+
cumulative = 0
|
|
122
|
+
hist.keys.sort.each do |value|
|
|
123
|
+
cumulative += hist[value]
|
|
124
|
+
return value if cumulative >= rank
|
|
125
|
+
end
|
|
126
|
+
hist.keys.max
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def reset
|
|
130
|
+
@mutex.synchronize do
|
|
131
|
+
@counts.clear
|
|
132
|
+
@distributions.clear
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -13,11 +13,8 @@ module Rigor
|
|
|
13
13
|
# `ary_push_internal`, …) that the classifier does not yet
|
|
14
14
|
# recognise. The blocklist captures the methods we have
|
|
15
15
|
# specifically observed flowing as `:leaf` despite mutating.
|
|
16
|
-
ARRAY_CATALOG = MethodCatalog.
|
|
17
|
-
|
|
18
|
-
"../../../../data/builtins/ruby_core/array.yml",
|
|
19
|
-
__dir__
|
|
20
|
-
),
|
|
16
|
+
ARRAY_CATALOG = MethodCatalog.for_topic(
|
|
17
|
+
"array",
|
|
21
18
|
mutating_selectors: {
|
|
22
19
|
"Array" => Set[
|
|
23
20
|
# Mutators classified `:leaf` by the C-body heuristic
|
|
@@ -13,11 +13,8 @@ module Rigor
|
|
|
13
13
|
# (which dispatches on the receiver's concrete class).
|
|
14
14
|
# The data is consumed by future include-aware lookup —
|
|
15
15
|
# see `docs/CURRENT_WORK.md` for the planned slice.
|
|
16
|
-
COMPARABLE_CATALOG = MethodCatalog.
|
|
17
|
-
|
|
18
|
-
"../../../../data/builtins/ruby_core/comparable.yml",
|
|
19
|
-
__dir__
|
|
20
|
-
),
|
|
16
|
+
COMPARABLE_CATALOG = MethodCatalog.for_topic(
|
|
17
|
+
"comparable",
|
|
21
18
|
mutating_selectors: {
|
|
22
19
|
"Comparable" => Set[]
|
|
23
20
|
}
|
|
@@ -21,11 +21,8 @@ module Rigor
|
|
|
21
21
|
# so a hypothetical future `Constant<Complex>` carrier cannot
|
|
22
22
|
# fold an aliasing copy through the catalog (mirrors
|
|
23
23
|
# `range_catalog.rb`, `time_catalog.rb`, `date_catalog.rb`).
|
|
24
|
-
COMPLEX_CATALOG = MethodCatalog.
|
|
25
|
-
|
|
26
|
-
"../../../../data/builtins/ruby_core/complex.yml",
|
|
27
|
-
__dir__
|
|
28
|
-
),
|
|
24
|
+
COMPLEX_CATALOG = MethodCatalog.for_topic(
|
|
25
|
+
"complex",
|
|
29
26
|
mutating_selectors: {
|
|
30
27
|
"Complex" => Set[
|
|
31
28
|
# Defence in depth: `Complex` does not currently expose
|
|
@@ -54,11 +54,8 @@ module Rigor
|
|
|
54
54
|
# blocklist entry; the entries below are defense-in-depth
|
|
55
55
|
# against indirect mutators the regex might miss in a future
|
|
56
56
|
# CRuby bump.
|
|
57
|
-
DATE_CATALOG = MethodCatalog.
|
|
58
|
-
|
|
59
|
-
"../../../../data/builtins/ruby_core/date.yml",
|
|
60
|
-
__dir__
|
|
61
|
-
),
|
|
57
|
+
DATE_CATALOG = MethodCatalog.for_topic(
|
|
58
|
+
"date",
|
|
62
59
|
mutating_selectors: {
|
|
63
60
|
"Date" => Set[
|
|
64
61
|
# `d_lite_initialize_copy` is already classed
|
|
@@ -22,11 +22,8 @@ module Rigor
|
|
|
22
22
|
# against the analyzer process's registry — what UTF-8's
|
|
23
23
|
# alias list is in the analyzer is not necessarily what it
|
|
24
24
|
# is in the analysed program.
|
|
25
|
-
ENCODING_CATALOG = MethodCatalog.
|
|
26
|
-
|
|
27
|
-
"../../../../data/builtins/ruby_core/encoding.yml",
|
|
28
|
-
__dir__
|
|
29
|
-
),
|
|
25
|
+
ENCODING_CATALOG = MethodCatalog.for_topic(
|
|
26
|
+
"encoding",
|
|
30
27
|
mutating_selectors: {
|
|
31
28
|
"Encoding" => Set[
|
|
32
29
|
# Defence-in-depth: mirrors range_catalog.rb /
|
|
@@ -13,11 +13,8 @@ module Rigor
|
|
|
13
13
|
# (which dispatches on the receiver's concrete class).
|
|
14
14
|
# The data is consumed by future include-aware lookup —
|
|
15
15
|
# see `docs/CURRENT_WORK.md` for the planned slice.
|
|
16
|
-
ENUMERABLE_CATALOG = MethodCatalog.
|
|
17
|
-
|
|
18
|
-
"../../../../data/builtins/ruby_core/enumerable.yml",
|
|
19
|
-
__dir__
|
|
20
|
-
),
|
|
16
|
+
ENUMERABLE_CATALOG = MethodCatalog.for_topic(
|
|
17
|
+
"enumerable",
|
|
21
18
|
mutating_selectors: {
|
|
22
19
|
"Enumerable" => Set[]
|
|
23
20
|
}
|
|
@@ -26,11 +26,8 @@ module Rigor
|
|
|
26
26
|
# already declines) or blocklisted because the static classifier
|
|
27
27
|
# missed an indirect side effect. The remaining `:leaf` method
|
|
28
28
|
# that DOES fold is `#cause`, a pure accessor.
|
|
29
|
-
EXCEPTION_CATALOG = MethodCatalog.
|
|
30
|
-
|
|
31
|
-
"../../../../data/builtins/ruby_core/exception.yml",
|
|
32
|
-
__dir__
|
|
33
|
-
),
|
|
29
|
+
EXCEPTION_CATALOG = MethodCatalog.for_topic(
|
|
30
|
+
"exception",
|
|
34
31
|
mutating_selectors: {
|
|
35
32
|
"Exception" => Set[
|
|
36
33
|
# `exc_initialize` writes `mesg` / `backtrace` ivars on
|
|
@@ -15,11 +15,8 @@ module Rigor
|
|
|
15
15
|
# captures every false-positive `:leaf` we have spotted in the
|
|
16
16
|
# generated YAML — bias toward conservatism so a missed fold is
|
|
17
17
|
# acceptable but a folded mutator/yielder is not.
|
|
18
|
-
HASH_CATALOG = MethodCatalog.
|
|
19
|
-
|
|
20
|
-
"../../../../data/builtins/ruby_core/hash.yml",
|
|
21
|
-
__dir__
|
|
22
|
-
),
|
|
18
|
+
HASH_CATALOG = MethodCatalog.for_topic(
|
|
19
|
+
"hash",
|
|
23
20
|
mutating_selectors: {
|
|
24
21
|
"Hash" => Set[
|
|
25
22
|
# Block-dependent iteration — yields via `rb_hash_foreach`
|