rigortype 0.1.16 → 0.1.18
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/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
- data/lib/rigor/analysis/check_rules.rb +180 -73
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +18 -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/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +477 -1110
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +50 -49
- data/lib/rigor/cache/incremental_snapshot.rb +153 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +34 -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_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +145 -14
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- 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/diagnostic_formats.rb +345 -0
- 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 +2 -7
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- 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/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- 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 +15 -532
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +16 -3
- data/lib/rigor/environment/rbs_loader.rb +129 -71
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- 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 -63
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- 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 +33 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
- 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 +185 -84
- data/lib/rigor/inference/narrowing.rb +262 -5
- data/lib/rigor/inference/scope_indexer.rb +208 -21
- data/lib/rigor/inference/statement_evaluator.rb +110 -48
- 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 +61 -38
- data/lib/rigor/plugin/base.rb +302 -45
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +281 -15
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +150 -167
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -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 +22 -0
- 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 +5 -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 +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +27 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +42 -25
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +6 -1
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +36 -2
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
|
@@ -1,104 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "method_catalog"
|
|
4
4
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Inference
|
|
7
7
|
module Builtins
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# produced offline by `tool/extract_numeric_catalog.rb` from the
|
|
11
|
-
# CRuby reference checkout under `references/ruby` plus the RBS
|
|
12
|
-
# core signatures under `references/rbs`.
|
|
8
|
+
# `Numeric` family catalog (Integer, Float, Rational, Complex,
|
|
9
|
+
# Numeric). Singleton — load once, consult during dispatch.
|
|
13
10
|
#
|
|
14
|
-
# The
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
11
|
+
# The catalog is produced offline by `tool/extract_builtin_catalog.rb`
|
|
12
|
+
# from the CRuby reference checkout under `references/ruby` plus the
|
|
13
|
+
# RBS core signatures under `references/rbs`. The loader is the
|
|
14
|
+
# runtime bridge: callers ask "is `Integer#+` safe to invoke during
|
|
15
|
+
# constant folding?" and the answer comes straight from the offline
|
|
16
|
+
# classification (`leaf` / `trivial` / `leaf_when_numeric` are
|
|
17
|
+
# foldable; everything else is not).
|
|
18
18
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# only lets it through when every argument is itself a
|
|
31
|
-
# `Constant<Numeric>` or `IntegerRange` — exactly the gate
|
|
32
|
-
# the catalog tag is named for.
|
|
33
|
-
FOLDABLE_PURITIES = Set["leaf", "trivial", "leaf_when_numeric"].freeze
|
|
34
|
-
|
|
35
|
-
EMPTY_CATALOG = { "classes" => {} }.freeze
|
|
36
|
-
private_constant :EMPTY_CATALOG
|
|
37
|
-
|
|
38
|
-
# Path resolved relative to this file. The catalog ships under
|
|
39
|
-
# `data/builtins/ruby_core/numeric.yml` at the gem root.
|
|
40
|
-
CATALOG_PATH = File.expand_path(
|
|
41
|
-
"../../../../data/builtins/ruby_core/numeric.yml",
|
|
42
|
-
__dir__
|
|
43
|
-
)
|
|
44
|
-
private_constant :CATALOG_PATH
|
|
45
|
-
|
|
46
|
-
class << self
|
|
47
|
-
# @param class_name [String] e.g. "Integer", "Float"
|
|
48
|
-
# @param selector [Symbol, String]
|
|
49
|
-
# @param kind [Symbol] :instance (default) or :singleton
|
|
50
|
-
# @return [Boolean]
|
|
51
|
-
def safe_for_folding?(class_name, selector, kind: :instance)
|
|
52
|
-
entry = method_entry(class_name, selector, kind: kind)
|
|
53
|
-
return false unless entry
|
|
54
|
-
|
|
55
|
-
FOLDABLE_PURITIES.include?(entry["purity"])
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# @return [Hash, nil] catalog entry for the given method, or
|
|
59
|
-
# nil when the method is not registered.
|
|
60
|
-
def method_entry(class_name, selector, kind: :instance)
|
|
61
|
-
klass = catalog.dig("classes", class_name.to_s)
|
|
62
|
-
return nil unless klass
|
|
63
|
-
|
|
64
|
-
bucket_key = kind == :singleton ? "singleton_methods" : "instance_methods"
|
|
65
|
-
klass.dig(bucket_key, selector.to_s)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Used by tests to drop the cached catalog so a different
|
|
69
|
-
# path or content can be exercised. Production code MUST
|
|
70
|
-
# NOT call this during normal operation.
|
|
71
|
-
#
|
|
72
|
-
# ADR-15 Phase 4b.x — reset re-loads eagerly so the
|
|
73
|
-
# singleton-class `@catalog` ivar stays populated, and
|
|
74
|
-
# the loaded Hash is deep-shared via `Ractor.make_shareable`
|
|
75
|
-
# so a worker Ractor reading the ivar via `catalog.dig(...)`
|
|
76
|
-
# does not trip `Ractor::IsolationError`. Plain `.freeze`
|
|
77
|
-
# is insufficient: YAML parses to a nested Hash/Array/String
|
|
78
|
-
# graph and only the outer Hash would be frozen.
|
|
79
|
-
def reset!
|
|
80
|
-
@catalog = Ractor.make_shareable(load_catalog)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
private
|
|
84
|
-
|
|
85
|
-
attr_reader :catalog
|
|
86
|
-
|
|
87
|
-
def load_catalog
|
|
88
|
-
return EMPTY_CATALOG unless File.exist?(CATALOG_PATH)
|
|
89
|
-
|
|
90
|
-
data = YAML.safe_load_file(CATALOG_PATH, permitted_classes: [Symbol])
|
|
91
|
-
data.is_a?(Hash) ? data : EMPTY_CATALOG
|
|
92
|
-
rescue Psych::SyntaxError
|
|
93
|
-
EMPTY_CATALOG
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# ADR-15 Phase 4b.x — eager-load on the main Ractor at
|
|
98
|
-
# module-load time so worker Ractors only READ the
|
|
99
|
-
# populated singleton-class `@catalog` ivar.
|
|
100
|
-
reset!
|
|
101
|
-
end
|
|
19
|
+
# No mutation blocklist is needed. The numeric classes expose no
|
|
20
|
+
# foldable bang or indirect-mutator method that the static C
|
|
21
|
+
# classifier mis-attributes (every `:leaf` numeric method is a pure
|
|
22
|
+
# value computation), so the generic `MethodCatalog` loader — shared
|
|
23
|
+
# with the eighteen other per-class catalogs — covers it directly.
|
|
24
|
+
# This previously hand-rolled its own `safe_for_folding?` /
|
|
25
|
+
# `method_entry` / `load_catalog` copy of `MethodCatalog`; folding
|
|
26
|
+
# it onto the shared loader also picks up alias resolution (e.g.
|
|
27
|
+
# `Integer#magnitude` → `abs`, `Integer#inspect` → `to_s`), which the
|
|
28
|
+
# old bespoke loader silently dropped.
|
|
29
|
+
NUMERIC_CATALOG = MethodCatalog.for_topic("numeric")
|
|
102
30
|
end
|
|
103
31
|
end
|
|
104
32
|
end
|
|
@@ -16,11 +16,8 @@ module Rigor
|
|
|
16
16
|
# helper that triggered the false positive (see
|
|
17
17
|
# `string_catalog.rb`, `array_catalog.rb`, `time_catalog.rb`
|
|
18
18
|
# for the canonical shape).
|
|
19
|
-
PATHNAME_CATALOG = MethodCatalog.
|
|
20
|
-
|
|
21
|
-
"../../../../data/builtins/ruby_core/pathname.yml",
|
|
22
|
-
__dir__
|
|
23
|
-
),
|
|
19
|
+
PATHNAME_CATALOG = MethodCatalog.for_topic(
|
|
20
|
+
"pathname",
|
|
24
21
|
mutating_selectors: {
|
|
25
22
|
"Pathname" => Set[
|
|
26
23
|
# initialize_copy is blocklisted by convention so a
|
|
@@ -31,11 +31,8 @@ module Rigor
|
|
|
31
31
|
# `#source_location`, `#name`, `#owner`, `#receiver`) remain
|
|
32
32
|
# foldable; the RBS tier still resolves return types for
|
|
33
33
|
# the blocklisted methods so callers do not lose precision.
|
|
34
|
-
PROC_CATALOG = MethodCatalog.
|
|
35
|
-
|
|
36
|
-
"../../../../data/builtins/ruby_core/proc.yml",
|
|
37
|
-
__dir__
|
|
38
|
-
),
|
|
34
|
+
PROC_CATALOG = MethodCatalog.for_topic(
|
|
35
|
+
"proc",
|
|
39
36
|
mutating_selectors: {
|
|
40
37
|
"Proc" => Set[
|
|
41
38
|
# `#call` / `#[]` / `#===` / `#yield` invoke the proc
|
|
@@ -21,11 +21,8 @@ module Rigor
|
|
|
21
21
|
# functionally pure they would produce a misleading constant
|
|
22
22
|
# at fold time. The whole class is conservative-by-default
|
|
23
23
|
# at the catalog tier; precision flows through the RBS layer.
|
|
24
|
-
RANDOM_CATALOG = MethodCatalog.
|
|
25
|
-
|
|
26
|
-
"../../../../data/builtins/ruby_core/random.yml",
|
|
27
|
-
__dir__
|
|
28
|
-
),
|
|
24
|
+
RANDOM_CATALOG = MethodCatalog.for_topic(
|
|
25
|
+
"random",
|
|
29
26
|
mutating_selectors: {
|
|
30
27
|
"Random" => Set[
|
|
31
28
|
# `rand_random` -> `random_real` / `random_ulong_limited`
|
|
@@ -15,11 +15,8 @@ module Rigor
|
|
|
15
15
|
# routes through a helper the block/yield regex does not
|
|
16
16
|
# recognise, so the classifier mis-flags them as `:leaf`
|
|
17
17
|
# despite yielding to a block.
|
|
18
|
-
RANGE_CATALOG = MethodCatalog.
|
|
19
|
-
|
|
20
|
-
"../../../../data/builtins/ruby_core/range.yml",
|
|
21
|
-
__dir__
|
|
22
|
-
),
|
|
18
|
+
RANGE_CATALOG = MethodCatalog.for_topic(
|
|
19
|
+
"range",
|
|
23
20
|
mutating_selectors: {
|
|
24
21
|
"Range" => Set[
|
|
25
22
|
# `range_initialize` / `range_initialize_copy` write
|
|
@@ -22,11 +22,8 @@ module Rigor
|
|
|
22
22
|
# hypothetical future `Constant<Rational>` carrier cannot
|
|
23
23
|
# fold an aliasing copy through the catalog and surface a
|
|
24
24
|
# shared mutable handle.
|
|
25
|
-
RATIONAL_CATALOG = MethodCatalog.
|
|
26
|
-
|
|
27
|
-
"../../../../data/builtins/ruby_core/rational.yml",
|
|
28
|
-
__dir__
|
|
29
|
-
),
|
|
25
|
+
RATIONAL_CATALOG = MethodCatalog.for_topic(
|
|
26
|
+
"rational",
|
|
30
27
|
mutating_selectors: {
|
|
31
28
|
"Rational" => Set[
|
|
32
29
|
:initialize_copy
|
|
@@ -38,11 +38,8 @@ module Rigor
|
|
|
38
38
|
# signatures already widen the answer enough to keep the
|
|
39
39
|
# behaviour sound; revisit if the dispatcher ever grows a
|
|
40
40
|
# singleton-aware catalog path.
|
|
41
|
-
REGEXP_CATALOG = MethodCatalog.
|
|
42
|
-
|
|
43
|
-
"../../../../data/builtins/ruby_core/re.yml",
|
|
44
|
-
__dir__
|
|
45
|
-
),
|
|
41
|
+
REGEXP_CATALOG = MethodCatalog.for_topic(
|
|
42
|
+
"re",
|
|
46
43
|
mutating_selectors: {
|
|
47
44
|
"Regexp" => Set[
|
|
48
45
|
# Defensive: aliasing-copy semantics already covered
|
|
@@ -19,11 +19,8 @@ module Rigor
|
|
|
19
19
|
# (`set_iter`, `RETURN_SIZED_ENUMERATOR`) and its identity-
|
|
20
20
|
# mode and reset paths drive into helpers the regex classifier
|
|
21
21
|
# does not yet recognise as block-yielding or mutating.
|
|
22
|
-
SET_CATALOG = MethodCatalog.
|
|
23
|
-
|
|
24
|
-
"../../../../data/builtins/ruby_core/set.yml",
|
|
25
|
-
__dir__
|
|
26
|
-
),
|
|
22
|
+
SET_CATALOG = MethodCatalog.for_topic(
|
|
23
|
+
"set",
|
|
27
24
|
mutating_selectors: {
|
|
28
25
|
"Set" => Set[
|
|
29
26
|
# Indirect mutators classified `:leaf` because the C
|
|
@@ -15,11 +15,8 @@ module Rigor
|
|
|
15
15
|
# mutation primitives). Adding to the blocklist is the
|
|
16
16
|
# corrective surface for false positives until the
|
|
17
17
|
# classifier learns the helper functions.
|
|
18
|
-
STRING_CATALOG = MethodCatalog.
|
|
19
|
-
|
|
20
|
-
"../../../../data/builtins/ruby_core/string.yml",
|
|
21
|
-
__dir__
|
|
22
|
-
),
|
|
18
|
+
STRING_CATALOG = MethodCatalog.for_topic(
|
|
19
|
+
"string",
|
|
23
20
|
mutating_selectors: {
|
|
24
21
|
"String" => Set[
|
|
25
22
|
:replace, :initialize, :initialize_copy, :clear, :<<, :concat, :insert,
|
|
@@ -23,11 +23,8 @@ module Rigor
|
|
|
23
23
|
# member but the answer depends on the subclass's member
|
|
24
24
|
# definition, which the catalog does not see, so we blocklist
|
|
25
25
|
# it defensively.
|
|
26
|
-
STRUCT_CATALOG = MethodCatalog.
|
|
27
|
-
|
|
28
|
-
"../../../../data/builtins/ruby_core/struct.yml",
|
|
29
|
-
__dir__
|
|
30
|
-
),
|
|
26
|
+
STRUCT_CATALOG = MethodCatalog.for_topic(
|
|
27
|
+
"struct",
|
|
31
28
|
mutating_selectors: {
|
|
32
29
|
"Struct" => Set[
|
|
33
30
|
# Defensive: aliasing-copy semantics on a hypothetical
|
|
@@ -29,11 +29,8 @@ module Rigor
|
|
|
29
29
|
# The blocklist captures the false-positive `:leaf` entries
|
|
30
30
|
# whose helper functions the regex classifier did not
|
|
31
31
|
# recognise as mutators.
|
|
32
|
-
TIME_CATALOG = MethodCatalog.
|
|
33
|
-
|
|
34
|
-
"../../../../data/builtins/ruby_core/time.yml",
|
|
35
|
-
__dir__
|
|
36
|
-
),
|
|
32
|
+
TIME_CATALOG = MethodCatalog.for_topic(
|
|
33
|
+
"time",
|
|
37
34
|
mutating_selectors: {
|
|
38
35
|
"Time" => Set[
|
|
39
36
|
# `time_init_copy` writes the `timew` and `vtm` slots on
|
|
@@ -4,9 +4,11 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "../ast"
|
|
7
|
+
require_relative "../analysis/self_call_resolution_recorder"
|
|
7
8
|
require_relative "block_parameter_binder"
|
|
8
9
|
require_relative "budget_trace"
|
|
9
10
|
require_relative "fallback"
|
|
11
|
+
require_relative "flow_tracer"
|
|
10
12
|
require_relative "indexed_narrowing"
|
|
11
13
|
require_relative "macro_block_self_type"
|
|
12
14
|
require_relative "method_dispatcher"
|
|
@@ -111,7 +113,7 @@ module Rigor
|
|
|
111
113
|
Prism::GlobalVariableOperatorWriteNode => :type_of_assignment_write,
|
|
112
114
|
Prism::GlobalVariableOrWriteNode => :type_of_assignment_write,
|
|
113
115
|
Prism::GlobalVariableAndWriteNode => :type_of_assignment_write,
|
|
114
|
-
# Compound writes that share the
|
|
116
|
+
# Compound writes that share the `.value` rvalue accessor
|
|
115
117
|
Prism::LocalVariableOperatorWriteNode => :type_of_assignment_write,
|
|
116
118
|
Prism::LocalVariableOrWriteNode => :type_of_assignment_write,
|
|
117
119
|
Prism::LocalVariableAndWriteNode => :type_of_assignment_write,
|
|
@@ -218,6 +220,15 @@ module Rigor
|
|
|
218
220
|
end
|
|
219
221
|
|
|
220
222
|
def type_of(node)
|
|
223
|
+
return untraced_type_of(node) unless FlowTracer.active?
|
|
224
|
+
|
|
225
|
+
# `rigor trace` — bracket the recursion with enter/result events.
|
|
226
|
+
# The tracer is observational only: the inferred type flows
|
|
227
|
+
# through unchanged (see FlowTracer's contract).
|
|
228
|
+
FlowTracer.trace_node(node) { untraced_type_of(node) }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def untraced_type_of(node)
|
|
221
232
|
# Slice A-declarations. ScopeIndexer pre-fills
|
|
222
233
|
# `scope.declared_types` for declaration-position nodes
|
|
223
234
|
# (`module Foo` / `class Bar` headers) with the qualified
|
|
@@ -443,8 +454,12 @@ module Rigor
|
|
|
443
454
|
candidates = []
|
|
444
455
|
while prefix && !prefix.empty?
|
|
445
456
|
candidates << "#{prefix}::#{name}"
|
|
446
|
-
|
|
447
|
-
|
|
457
|
+
# Strip the last `::` segment without `rpartition`'s throwaway
|
|
458
|
+
# 3-element array + extra substrings (this loop is the sole
|
|
459
|
+
# caller of the `String#rpartition` allocation seen in the
|
|
460
|
+
# profile): `rindex` + slice gives the same prefix, or nil.
|
|
461
|
+
idx = prefix.rindex("::")
|
|
462
|
+
prefix = idx ? prefix[0, idx] : nil
|
|
448
463
|
end
|
|
449
464
|
candidates << name
|
|
450
465
|
candidates
|
|
@@ -663,26 +678,17 @@ module Rigor
|
|
|
663
678
|
end
|
|
664
679
|
|
|
665
680
|
# Returns `:truthy`, `:falsey`, or `nil` for an arbitrary
|
|
666
|
-
# predicate expression under three-valued logic.
|
|
667
|
-
#
|
|
668
|
-
#
|
|
669
|
-
#
|
|
670
|
-
# `
|
|
671
|
-
#
|
|
672
|
-
#
|
|
681
|
+
# predicate expression under three-valued logic.
|
|
682
|
+
# {Narrowing.predicate_certainty} owns the judgment (the same
|
|
683
|
+
# one `StatementEvaluator#live_branch_for_if` reads on the
|
|
684
|
+
# scope side): `Nominal[Integer]` (always truthy in Ruby),
|
|
685
|
+
# `Constant[nil]`, and `Constant[false]` fold one branch;
|
|
686
|
+
# `Union[true, false]`, `Dynamic[T]`, and `Top` keep both
|
|
687
|
+
# branches live.
|
|
673
688
|
def constant_predicate_polarity(predicate)
|
|
674
689
|
return nil if predicate.nil?
|
|
675
690
|
|
|
676
|
-
|
|
677
|
-
return nil if type.nil? || type.is_a?(Type::Bot)
|
|
678
|
-
|
|
679
|
-
truthy_bot = Narrowing.narrow_truthy(type).is_a?(Type::Bot)
|
|
680
|
-
falsey_bot = Narrowing.narrow_falsey(type).is_a?(Type::Bot)
|
|
681
|
-
|
|
682
|
-
return :falsey if truthy_bot && !falsey_bot
|
|
683
|
-
return :truthy if !truthy_bot && falsey_bot
|
|
684
|
-
|
|
685
|
-
nil
|
|
691
|
+
Narrowing.predicate_certainty(type_of(predicate))
|
|
686
692
|
end
|
|
687
693
|
|
|
688
694
|
def type_of_else(node)
|
|
@@ -813,29 +819,14 @@ module Rigor
|
|
|
813
819
|
# `:maybe` — the existing union fallback handles them.
|
|
814
820
|
def case_when_pattern_certainty(subject_type, pattern_node)
|
|
815
821
|
class_name = build_constant_path_name(pattern_node)
|
|
816
|
-
return class_pattern_certainty(subject_type, class_name) if class_name
|
|
822
|
+
return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
|
|
817
823
|
|
|
818
824
|
literal = literal_pattern_value(pattern_node)
|
|
819
|
-
return
|
|
820
|
-
|
|
821
|
-
:maybe
|
|
822
|
-
end
|
|
823
|
-
|
|
824
|
-
def class_pattern_certainty(subject_type, class_name)
|
|
825
|
-
env = scope.environment
|
|
826
|
-
truthy_bot = Narrowing.narrow_class(subject_type, class_name, environment: env).is_a?(Type::Bot)
|
|
827
|
-
falsey_bot = Narrowing.narrow_not_class(subject_type, class_name, environment: env).is_a?(Type::Bot)
|
|
828
|
-
|
|
829
|
-
return :no if truthy_bot && !falsey_bot
|
|
830
|
-
return :yes if !truthy_bot && falsey_bot
|
|
825
|
+
return Narrowing.value_pattern_certainty(subject_type, literal[:value]) if literal
|
|
831
826
|
|
|
832
827
|
:maybe
|
|
833
828
|
end
|
|
834
829
|
|
|
835
|
-
VALUE_EQUALITY_CLASSES = [Integer, Float, Rational, Complex, String, Symbol,
|
|
836
|
-
TrueClass, FalseClass, NilClass].freeze
|
|
837
|
-
private_constant :VALUE_EQUALITY_CLASSES
|
|
838
|
-
|
|
839
830
|
# Returns `{ value: v }` when `pattern_node` types to a
|
|
840
831
|
# `Constant[v]` of a value-equality-safe class (so `===`
|
|
841
832
|
# reduces to `==`), else nil. Wrapped in a hash so a literal
|
|
@@ -844,18 +835,11 @@ module Rigor
|
|
|
844
835
|
def literal_pattern_value(pattern_node)
|
|
845
836
|
type = type_of(pattern_node)
|
|
846
837
|
return nil unless type.is_a?(Type::Constant)
|
|
847
|
-
return nil unless VALUE_EQUALITY_CLASSES.any? { |klass| type.value.is_a?(klass) }
|
|
838
|
+
return nil unless Narrowing::VALUE_EQUALITY_CLASSES.any? { |klass| type.value.is_a?(klass) }
|
|
848
839
|
|
|
849
840
|
{ value: type.value }
|
|
850
841
|
end
|
|
851
842
|
|
|
852
|
-
def literal_pattern_certainty(subject_type, pattern_value)
|
|
853
|
-
return :maybe unless subject_type.is_a?(Type::Constant)
|
|
854
|
-
return :maybe unless VALUE_EQUALITY_CLASSES.any? { |klass| subject_type.value.is_a?(klass) }
|
|
855
|
-
|
|
856
|
-
pattern_value == subject_type.value ? :yes : :no
|
|
857
|
-
end
|
|
858
|
-
|
|
859
843
|
# `when` clauses for `case` and `in` clauses for `case ... in` have
|
|
860
844
|
# the same body shape; we reuse one handler for both Prism node
|
|
861
845
|
# classes.
|
|
@@ -1260,9 +1244,64 @@ module Rigor
|
|
|
1260
1244
|
# MUST NOT record a tracer event.
|
|
1261
1245
|
return dynamic_top if receiver.is_a?(Type::Dynamic)
|
|
1262
1246
|
|
|
1247
|
+
# ADR-24 slice 4a — this is the engine choke-point where an
|
|
1248
|
+
# implicit-self call has exhausted every resolution tier (RBS
|
|
1249
|
+
# dispatch + user-class ancestor walk) and falls through to
|
|
1250
|
+
# `Dynamic[top]`. When the slice-4 recorder is active, capture the
|
|
1251
|
+
# miss so a later slice's closed-class gate can flag it. Off by
|
|
1252
|
+
# default: `active?` is a plain integer read.
|
|
1253
|
+
record_unresolved_self_call(node, receiver) if Analysis::SelfCallResolutionRecorder.active?
|
|
1254
|
+
|
|
1263
1255
|
fallback_for(node, family: :prism)
|
|
1264
1256
|
end
|
|
1265
1257
|
|
|
1258
|
+
# ADR-24 slice 4a — records an unresolved *implicit-self* call (no
|
|
1259
|
+
# explicit receiver) whose `self` types to a concrete user class.
|
|
1260
|
+
# Explicit-receiver misses are out of scope (the existing
|
|
1261
|
+
# `call.undefined-method` rule already owns receiver-typed dispatch);
|
|
1262
|
+
# a non-`Nominal` self (top-level / DSL-block `self`, or a `Dynamic`
|
|
1263
|
+
# self) is skipped so the gradual guarantee is never touched here.
|
|
1264
|
+
def record_unresolved_self_call(node, receiver)
|
|
1265
|
+
return unless node.receiver.nil?
|
|
1266
|
+
return unless receiver.is_a?(Type::Nominal)
|
|
1267
|
+
return if self_call_method_known?(receiver.class_name, node.name)
|
|
1268
|
+
|
|
1269
|
+
location = node.message_loc || node.location
|
|
1270
|
+
Analysis::SelfCallResolutionRecorder.record(
|
|
1271
|
+
class_name: receiver.class_name,
|
|
1272
|
+
method_name: node.name,
|
|
1273
|
+
node: node,
|
|
1274
|
+
path: scope.source_path,
|
|
1275
|
+
line: location&.start_line,
|
|
1276
|
+
column: location ? location.start_column + 1 : nil
|
|
1277
|
+
)
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
# The recorder must capture *existence* misses, not type misses.
|
|
1281
|
+
# Reaching the choke-point means RBS dispatch produced no result, but
|
|
1282
|
+
# a project method can still EXIST without an inferable return type —
|
|
1283
|
+
# a `module_function` sibling whose body the engine can't fully type,
|
|
1284
|
+
# an `attr_reader` / `define_method` / `Data.define` member. Recording
|
|
1285
|
+
# those would reproduce the 135 false positives of slice-4 attempt 1.
|
|
1286
|
+
# So skip any name the engine's own existence signals already know:
|
|
1287
|
+
# a `def` resolvable through the ancestor walk, or an own-class entry
|
|
1288
|
+
# in the discovered-methods table (`def` / `attr_*` / `define_method`
|
|
1289
|
+
# / alias). This reuses the engine's real resolution — the
|
|
1290
|
+
# "collect, don't recompute" lesson — so only a name that exists
|
|
1291
|
+
# nowhere a project signal can see reaches the recorder.
|
|
1292
|
+
# `module_function` records its defs as `:singleton` (an implicit-self
|
|
1293
|
+
# call inside such a method dispatches to the module's singleton
|
|
1294
|
+
# method), while ordinary instance methods record `:instance`. The
|
|
1295
|
+
# recorder cannot tell the two contexts apart from the call node, so
|
|
1296
|
+
# existence under EITHER kind suppresses recording — the FP-safe
|
|
1297
|
+
# choice, since either means the method genuinely exists.
|
|
1298
|
+
def self_call_method_known?(class_name, method_name)
|
|
1299
|
+
return true if resolve_user_def_through_ancestors(class_name, method_name)
|
|
1300
|
+
|
|
1301
|
+
scope.discovered_method?(class_name, method_name, :instance) ||
|
|
1302
|
+
scope.discovered_method?(class_name, method_name, :singleton)
|
|
1303
|
+
end
|
|
1304
|
+
|
|
1266
1305
|
# v0.0.2 #5 — re-types the body of a user-defined
|
|
1267
1306
|
# instance method with the call site's argument types
|
|
1268
1307
|
# bound to the method's parameters. Used as a
|
|
@@ -1349,7 +1388,44 @@ module Rigor
|
|
|
1349
1388
|
ANCESTOR_WALK_LIMIT = 100
|
|
1350
1389
|
private_constant :ANCESTOR_WALK_LIMIT
|
|
1351
1390
|
|
|
1391
|
+
CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
|
|
1392
|
+
private_constant :CLASS_GRAPH_CACHE_KEY
|
|
1393
|
+
|
|
1394
|
+
# Run-scoped memo for the static class-graph resolvers below. They
|
|
1395
|
+
# are pure functions of the *frozen* project index trio
|
|
1396
|
+
# (`discovered_def_nodes` / `discovered_superclasses` /
|
|
1397
|
+
# `discovered_includes`) — `user_def_for` / `superclass_of` /
|
|
1398
|
+
# `includes_of` read nothing else, and never touch the current
|
|
1399
|
+
# scope's locals or narrowings — so a result computed for one
|
|
1400
|
+
# `(class, method)` is valid for every `Scope` that shares those
|
|
1401
|
+
# tables. `ExpressionTyper` is rebuilt per `Scope#type_of`, so the
|
|
1402
|
+
# memo lives on `Thread.current` rather than on `self`. It is keyed
|
|
1403
|
+
# by the *identity* of the three frozen tables (nested
|
|
1404
|
+
# `compare_by_identity` stores): a new analysis generation, or any
|
|
1405
|
+
# `Scope` that swaps an index via `with_discovered_*`, transparently
|
|
1406
|
+
# lands in a fresh bucket while everything sharing the tables shares
|
|
1407
|
+
# the memo. Steady-state cost is three identity-keyed hash reads and
|
|
1408
|
+
# zero allocation — the `||=` chains only allocate on the first miss
|
|
1409
|
+
# of a generation. (Pool mode forks per worker, so the
|
|
1410
|
+
# `Thread.current` store is process-local and never crosses a
|
|
1411
|
+
# project boundary.)
|
|
1412
|
+
def class_graph_buckets
|
|
1413
|
+
store = (Thread.current[CLASS_GRAPH_CACHE_KEY] ||= {}.compare_by_identity)
|
|
1414
|
+
by_def = (store[scope.discovered_def_nodes] ||= {}.compare_by_identity)
|
|
1415
|
+
by_super = (by_def[scope.discovered_superclasses] ||= {}.compare_by_identity)
|
|
1416
|
+
by_super[scope.discovered_includes] ||= { name: {}, user_def: {} }
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1352
1419
|
def resolve_user_def_through_ancestors(class_name, method_name)
|
|
1420
|
+
cache = class_graph_buckets[:user_def]
|
|
1421
|
+
table = (cache[class_name.to_s] ||= {})
|
|
1422
|
+
key = method_name.to_sym
|
|
1423
|
+
return table[key] if table.key?(key)
|
|
1424
|
+
|
|
1425
|
+
table[key] = compute_user_def_through_ancestors(class_name, method_name)
|
|
1426
|
+
end
|
|
1427
|
+
|
|
1428
|
+
def compute_user_def_through_ancestors(class_name, method_name)
|
|
1353
1429
|
queue = [class_name.to_s]
|
|
1354
1430
|
seen = {}
|
|
1355
1431
|
visited = 0
|
|
@@ -1398,6 +1474,14 @@ module Rigor
|
|
|
1398
1474
|
# no candidate names a discovered user class (e.g. the
|
|
1399
1475
|
# superclass is an RBS-known or third-party class).
|
|
1400
1476
|
def resolve_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1477
|
+
by_subclass = (class_graph_buckets[:name][subclass_qualified] ||= {})
|
|
1478
|
+
return by_subclass[raw_superclass] if by_subclass.key?(raw_superclass)
|
|
1479
|
+
|
|
1480
|
+
by_subclass[raw_superclass] =
|
|
1481
|
+
compute_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1482
|
+
end
|
|
1483
|
+
|
|
1484
|
+
def compute_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1401
1485
|
segments = subclass_qualified.split("::")
|
|
1402
1486
|
(segments.length - 1).downto(0) do |i|
|
|
1403
1487
|
candidate = (segments[0, i] + [raw_superclass]).join("::")
|
|
@@ -1460,29 +1544,31 @@ module Rigor
|
|
|
1460
1544
|
# nil when the parameter shape is too complex for the
|
|
1461
1545
|
# first-iteration binder (rest args, keyword args,
|
|
1462
1546
|
# block params, etc.).
|
|
1463
|
-
def build_user_method_body_scope(def_node, receiver, arg_types)
|
|
1547
|
+
def build_user_method_body_scope(def_node, receiver, arg_types)
|
|
1464
1548
|
params = def_node.parameters
|
|
1465
1549
|
required = params&.requireds || []
|
|
1466
1550
|
return nil unless params.nil? || user_method_param_shape_simple?(params)
|
|
1467
1551
|
return nil unless required.size == arg_types.size
|
|
1468
1552
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1553
|
+
# Bind required positionals by index. The body scope starts from an
|
|
1554
|
+
# empty fact store and narrowing set, so `with_local`'s fact /
|
|
1555
|
+
# narrowing invalidations would be no-ops here — build the locals
|
|
1556
|
+
# table directly (matching `with_local`'s `name.to_sym` key).
|
|
1557
|
+
locals = {}
|
|
1558
|
+
required.each_with_index { |param, index| locals[param.name.to_sym] = arg_types[index] }
|
|
1559
|
+
|
|
1560
|
+
# Construct the body scope in a SINGLE allocation — the previous
|
|
1561
|
+
# `Scope.empty.with_*.with_*…` chain allocated a fresh frozen Scope
|
|
1562
|
+
# per field, run per user-method-call inference (ADR-44). The
|
|
1563
|
+
# discovery index is inherited whole by reference (ADR-53 Track A);
|
|
1564
|
+
# the hand-copied per-field list this replaces had silently dropped
|
|
1565
|
+
# `data_member_layouts` and `discovered_method_visibilities`.
|
|
1566
|
+
Scope.new(
|
|
1567
|
+
environment: scope.environment,
|
|
1568
|
+
locals: locals.freeze,
|
|
1569
|
+
self_type: receiver,
|
|
1570
|
+
discovery: scope.discovery
|
|
1571
|
+
)
|
|
1486
1572
|
end
|
|
1487
1573
|
|
|
1488
1574
|
# First iteration accepts only required positional
|