rigortype 0.1.18 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +74 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +1072 -71
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
- 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 +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
data/lib/rigor/scope.rb
CHANGED
|
@@ -22,7 +22,8 @@ module Rigor
|
|
|
22
22
|
attr_reader :environment, :locals, :fact_store, :self_type,
|
|
23
23
|
:ivars, :cvars, :globals,
|
|
24
24
|
:indexed_narrowings, :method_chain_narrowings,
|
|
25
|
-
:
|
|
25
|
+
:declaration_sourced,
|
|
26
|
+
:source_path, :discovery, :struct_fold_safe_locals
|
|
26
27
|
|
|
27
28
|
# ADR-53 Track A — the seed-time discovery tables live on the
|
|
28
29
|
# {DiscoveryIndex} the scope carries by a single reference; the
|
|
@@ -43,12 +44,20 @@ module Rigor
|
|
|
43
44
|
def in_source_constants = @discovery.in_source_constants
|
|
44
45
|
def discovered_methods = @discovery.discovered_methods
|
|
45
46
|
def discovered_def_nodes = @discovery.discovered_def_nodes
|
|
47
|
+
def discovered_singleton_def_nodes = @discovery.discovered_singleton_def_nodes
|
|
46
48
|
def discovered_def_sources = @discovery.discovered_def_sources
|
|
47
49
|
def discovered_method_visibilities = @discovery.discovered_method_visibilities
|
|
48
50
|
def discovered_superclasses = @discovery.discovered_superclasses
|
|
49
51
|
def discovered_includes = @discovery.discovered_includes
|
|
50
52
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
51
53
|
def data_member_layouts = @discovery.data_member_layouts
|
|
54
|
+
def struct_member_layouts = @discovery.struct_member_layouts
|
|
55
|
+
# ADR-67 WD3 — call-site-inferred parameter types, keyed by
|
|
56
|
+
# `[class_name, method_name, kind]`. `build_method_entry_scope` consults
|
|
57
|
+
# this to seed an undeclared `def` parameter with the union of its
|
|
58
|
+
# resolved call-site argument types (precision-additive; an RBS-declared
|
|
59
|
+
# parameter always wins). Empty unless a collection pass seeded it.
|
|
60
|
+
def param_inferred_types = @discovery.param_inferred_types
|
|
52
61
|
|
|
53
62
|
# Narrowing key for an indexed read `receiver[key]` where both
|
|
54
63
|
# the receiver and the key are stable enough to address. The
|
|
@@ -86,8 +95,25 @@ module Rigor
|
|
|
86
95
|
EMPTY_VAR_BINDINGS = {}.freeze
|
|
87
96
|
EMPTY_INDEXED_NARROWINGS = {}.freeze
|
|
88
97
|
EMPTY_CHAIN_NARROWINGS = {}.freeze
|
|
98
|
+
# ADR-58 WD1 — the set of variable references whose binding's `nil`
|
|
99
|
+
# constituent is *declaration-sourced*: it arrives only via the
|
|
100
|
+
# class-ivar index seed (a ctor `@x = nil` written in another method),
|
|
101
|
+
# never through a method-local write, narrowing, or parameter. Members
|
|
102
|
+
# are frozen `[kind, name]` pairs (`[:ivar, :@x]`, `[:local, :r]`).
|
|
103
|
+
# `possible-nil-receiver` consults this set and declines to fire when the
|
|
104
|
+
# receiver's optionality is purely declaration-sourced — the working
|
|
105
|
+
# program's cross-method invariant is assumed per the robustness
|
|
106
|
+
# principle. Any flow-live touch (write / narrowing) drops the mark, so
|
|
107
|
+
# the diagnostic keeps firing exactly as before on flow-observed nil.
|
|
108
|
+
EMPTY_DECLARATION_SOURCED = Set.new.freeze
|
|
109
|
+
# ADR-48 Struct slice 3 — the per-body set of local names whose struct
|
|
110
|
+
# member reads are fold-safe (provably never mutated / aliased / escaped).
|
|
111
|
+
# A static per-scope context like {#source_path}: inherited unchanged
|
|
112
|
+
# through flow transitions and ignored by `==` / `hash`.
|
|
113
|
+
EMPTY_FOLD_SAFE = Set.new.freeze
|
|
89
114
|
private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
|
|
90
|
-
:EMPTY_CHAIN_NARROWINGS
|
|
115
|
+
:EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
|
|
116
|
+
:EMPTY_FOLD_SAFE
|
|
91
117
|
|
|
92
118
|
class << self
|
|
93
119
|
def empty(environment: Environment.default, source_path: nil)
|
|
@@ -106,7 +132,9 @@ module Rigor
|
|
|
106
132
|
discovery: DiscoveryIndex::EMPTY,
|
|
107
133
|
indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
|
|
108
134
|
method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
|
|
109
|
-
|
|
135
|
+
declaration_sourced: EMPTY_DECLARATION_SOURCED,
|
|
136
|
+
source_path: nil,
|
|
137
|
+
struct_fold_safe_locals: EMPTY_FOLD_SAFE
|
|
110
138
|
)
|
|
111
139
|
@environment = environment
|
|
112
140
|
@locals = locals
|
|
@@ -118,7 +146,9 @@ module Rigor
|
|
|
118
146
|
@discovery = discovery
|
|
119
147
|
@indexed_narrowings = indexed_narrowings
|
|
120
148
|
@method_chain_narrowings = method_chain_narrowings
|
|
149
|
+
@declaration_sourced = declaration_sourced
|
|
121
150
|
@source_path = source_path
|
|
151
|
+
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
122
152
|
freeze
|
|
123
153
|
end
|
|
124
154
|
|
|
@@ -141,9 +171,15 @@ module Rigor
|
|
|
141
171
|
# narrowing keyed on `(local, :x, :last)` no longer holds.
|
|
142
172
|
new_indexed_narrowings = drop_indexed_narrowings_for(:local, name)
|
|
143
173
|
new_chain_narrowings = drop_chain_narrowings_for(:local, name)
|
|
174
|
+
# ADR-58 WD1 — rebinding a local is a flow-live touch: any prior
|
|
175
|
+
# declaration-sourced mark on `name` no longer holds (the new value
|
|
176
|
+
# may carry a method-local nil). `with_declaration_sourced_local`
|
|
177
|
+
# re-establishes the mark afterward when the RHS is a pure copy of a
|
|
178
|
+
# declaration-sourced ivar read; the default is to drop it.
|
|
144
179
|
rebuild(locals: new_locals, fact_store: new_fact_store,
|
|
145
180
|
indexed_narrowings: new_indexed_narrowings,
|
|
146
|
-
method_chain_narrowings: new_chain_narrowings
|
|
181
|
+
method_chain_narrowings: new_chain_narrowings,
|
|
182
|
+
declaration_sourced: drop_declaration_sourced_for(:local, name))
|
|
147
183
|
end
|
|
148
184
|
|
|
149
185
|
def with_fact(fact)
|
|
@@ -159,17 +195,29 @@ module Rigor
|
|
|
159
195
|
rebuild(self_type: type)
|
|
160
196
|
end
|
|
161
197
|
|
|
162
|
-
# ADR-
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
# file does this call site belong to?" without
|
|
198
|
+
# ADR-28 / ADR-52 slice 5a — per-file source path carried on
|
|
199
|
+
# the scope. The analyzer stamps the current file's path onto
|
|
200
|
+
# the seed scope; nested rebuilds propagate it so plugin rules
|
|
201
|
+
# (`dynamic_return`'s `file_methods:` gate, sigil checks) can
|
|
202
|
+
# resolve "which file does this call site belong to?" without
|
|
168
203
|
# thread-locals.
|
|
169
204
|
def with_source_path(path)
|
|
170
205
|
rebuild(source_path: path)
|
|
171
206
|
end
|
|
172
207
|
|
|
208
|
+
# ADR-48 Struct slice 3 — installs the per-body fold-safe-local set
|
|
209
|
+
# ({Inference::StructFoldSafety}). Set once at body entry; inherited
|
|
210
|
+
# unchanged through subsequent flow transitions.
|
|
211
|
+
def with_struct_fold_safe(locals)
|
|
212
|
+
rebuild(struct_fold_safe_locals: locals)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# True when `name`'s `Struct` member reads are fold-safe in this body
|
|
216
|
+
# (the local is provably never mutated / aliased / escaped).
|
|
217
|
+
def struct_fold_safe?(name)
|
|
218
|
+
@struct_fold_safe_locals.include?(name.to_sym)
|
|
219
|
+
end
|
|
220
|
+
|
|
173
221
|
# ADR-53 Track A — swaps the whole discovery index in one transition.
|
|
174
222
|
# The sole seeding path; the per-table writers it replaced are derived
|
|
175
223
|
# off-`Scope` through `scope.discovery.with(table_name: table)`.
|
|
@@ -201,9 +249,46 @@ module Rigor
|
|
|
201
249
|
def with_ivar(name, type)
|
|
202
250
|
new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
|
|
203
251
|
new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
|
|
252
|
+
# ADR-58 WD1 — a method-local ivar write or narrowing is flow-live:
|
|
253
|
+
# drop any declaration-sourced mark so subsequent reads of `@name`
|
|
254
|
+
# observe flow-live provenance and fire as before. The seed path uses
|
|
255
|
+
# `seed_declaration_sourced_ivar` to (re-)establish the mark.
|
|
204
256
|
rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
|
|
205
257
|
indexed_narrowings: new_indexed_narrowings,
|
|
206
|
-
method_chain_narrowings: new_chain_narrowings
|
|
258
|
+
method_chain_narrowings: new_chain_narrowings,
|
|
259
|
+
declaration_sourced: drop_declaration_sourced_for(:ivar, name))
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only
|
|
263
|
+
# provenance is the class-ivar index. Unlike `with_ivar` this binds the
|
|
264
|
+
# type AND records the declaration-sourced mark in one transition.
|
|
265
|
+
def seed_declaration_sourced_ivar(name, type)
|
|
266
|
+
rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
|
|
267
|
+
declaration_sourced: add_declaration_sourced(:ivar, name))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# ADR-58 WD1 — a local assignment `r = @right` whose RHS is a pure read
|
|
271
|
+
# of a declaration-sourced ivar inherits the mark, so the survey's exact
|
|
272
|
+
# rotation/traversal shape (`r = @right; r.key`) does not fire. Binds the
|
|
273
|
+
# type and stamps the local's mark in one transition (the plain
|
|
274
|
+
# `with_local` would have dropped it).
|
|
275
|
+
def with_declaration_sourced_local(name, type)
|
|
276
|
+
written = with_local(name, type)
|
|
277
|
+
written.with_local_declaration_mark(name)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# ADR-58 WD1 — re-stamp the local mark on a scope produced by
|
|
281
|
+
# `with_local` (which always drops it). Public so the sibling
|
|
282
|
+
# `with_declaration_sourced_local` can call it across the new
|
|
283
|
+
# post-write receiver without reaching into a private method.
|
|
284
|
+
def with_local_declaration_mark(name)
|
|
285
|
+
rebuild(declaration_sourced: add_declaration_sourced(:local, name))
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely
|
|
289
|
+
# declaration-sourced (no flow-live write/narrowing has touched it).
|
|
290
|
+
def declaration_sourced?(kind, name)
|
|
291
|
+
@declaration_sourced.include?([kind.to_sym, name.to_sym])
|
|
207
292
|
end
|
|
208
293
|
|
|
209
294
|
def with_cvar(name, type)
|
|
@@ -214,6 +299,23 @@ module Rigor
|
|
|
214
299
|
rebuild(globals: @globals.merge(name.to_sym => type).freeze)
|
|
215
300
|
end
|
|
216
301
|
|
|
302
|
+
# Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match
|
|
303
|
+
# and last-paren back-references). Narrowed on a successful-`=~` /
|
|
304
|
+
# `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`);
|
|
305
|
+
# any subsequent method call could run another match and rebind every
|
|
306
|
+
# one of them, so `eval_call` forgets the narrowed facts here. Always
|
|
307
|
+
# safe — only drops facts, so a subsequent read falls back to the
|
|
308
|
+
# default `String | nil`. Program-level `$GLOBAL = ...` seeds use other
|
|
309
|
+
# names and are untouched.
|
|
310
|
+
MATCH_DATA_GLOBALS = %i[$~ $& $` $' $+ $1 $2 $3 $4 $5 $6 $7 $8 $9].freeze
|
|
311
|
+
private_constant :MATCH_DATA_GLOBALS
|
|
312
|
+
|
|
313
|
+
def forget_match_globals
|
|
314
|
+
return self unless @globals.keys.any? { |k| MATCH_DATA_GLOBALS.include?(k) }
|
|
315
|
+
|
|
316
|
+
rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
|
|
317
|
+
end
|
|
318
|
+
|
|
217
319
|
# Slice 7 phase 2 — class-level ivar accumulator. Keyed by
|
|
218
320
|
# the qualified class name (e.g. `"Rigor::Scope"`); the
|
|
219
321
|
# value is a `Hash[Symbol, Type::t]` of every ivar that
|
|
@@ -263,8 +365,9 @@ module Rigor
|
|
|
263
365
|
# scope (no enclosing `class` / `module` body). True at the top
|
|
264
366
|
# of a file AND inside a top-level `def` body (since toplevel
|
|
265
367
|
# defs leave `self_type` nil per the existing scope-construction
|
|
266
|
-
# contract
|
|
267
|
-
#
|
|
368
|
+
# contract — the same nil-`self_type` signal ADR-24's self-call
|
|
369
|
+
# return adoption historically keyed on before ADR-57 opened the
|
|
370
|
+
# gate unconditionally). Used by
|
|
268
371
|
# `CheckRules#unresolved_toplevel_diagnostic` to gate the
|
|
269
372
|
# `call.unresolved-toplevel` rule so it fires only outside
|
|
270
373
|
# class / module bodies, where Rails-DSL metaprogramming
|
|
@@ -289,6 +392,22 @@ module Rigor
|
|
|
289
392
|
node
|
|
290
393
|
end
|
|
291
394
|
|
|
395
|
+
# Module-singleton call resolution (ADR-57 follow-up) — companion of
|
|
396
|
+
# {#user_def_for} for SINGLETON-side defs (`def self.x`, `def Foo.x`,
|
|
397
|
+
# `class << self` bodies, and `module_function` defs). Returns the
|
|
398
|
+
# `Prism::DefNode` for `class_name.method_name` invoked on the
|
|
399
|
+
# module/class constant itself, or nil. The `discovered_def_nodes`
|
|
400
|
+
# table is deliberately instance-side only (its ancestor walk binds
|
|
401
|
+
# `self` as `Nominal`), so singleton bodies live in a parallel table
|
|
402
|
+
# the `ScopeIndexer` populates alongside it. Records the same
|
|
403
|
+
# cross-file dependency edge as the instance path (ADR-46).
|
|
404
|
+
def singleton_def_for(class_name, method_name)
|
|
405
|
+
table = @discovery.discovered_singleton_def_nodes[class_name.to_s]
|
|
406
|
+
node = table && table[method_name.to_sym]
|
|
407
|
+
record_cross_file_method(class_name, method_name, node) if Analysis::DependencyRecorder.active?
|
|
408
|
+
node
|
|
409
|
+
end
|
|
410
|
+
|
|
292
411
|
# ADR-46 slice 1 — note the cross-file dependency this resolution
|
|
293
412
|
# creates: the file defining `class_name#method_name` (the consumer's
|
|
294
413
|
# analysis reads its body via `infer_user_method_return`), or, when
|
|
@@ -392,6 +511,20 @@ module Rigor
|
|
|
392
511
|
layout
|
|
393
512
|
end
|
|
394
513
|
|
|
514
|
+
# ADR-48 Struct follow-up — the `{ members:, keyword_init: }` layout
|
|
515
|
+
# recorded for a `Struct.new(...)`-defined class, in the constant form
|
|
516
|
+
# (`Point = Struct.new(:x, :y)`) and the named-subclass form
|
|
517
|
+
# (`class Point < Struct.new(:x, :y)`). Consumed by
|
|
518
|
+
# {Inference::MethodDispatcher::StructFolding} so `Point.new(...)` on a
|
|
519
|
+
# `Singleton[Point]` receiver materialises a member instance. Returns nil
|
|
520
|
+
# when the class has no recorded struct layout. Mirrors
|
|
521
|
+
# {#data_member_layout}'s dependency-recording contract.
|
|
522
|
+
def struct_member_layout(class_name)
|
|
523
|
+
layout = @discovery.struct_member_layouts[class_name.to_s]
|
|
524
|
+
record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
|
|
525
|
+
layout
|
|
526
|
+
end
|
|
527
|
+
|
|
395
528
|
# ADR-24 slice 2 — per-class/module table mapping a fully
|
|
396
529
|
# qualified user class or module to the list of module
|
|
397
530
|
# names it `include`s / `prepend`s, AS WRITTEN at the
|
|
@@ -564,7 +697,8 @@ module Rigor
|
|
|
564
697
|
@cvars == other.cvars &&
|
|
565
698
|
@globals == other.globals &&
|
|
566
699
|
@indexed_narrowings == other.indexed_narrowings &&
|
|
567
|
-
@method_chain_narrowings == other.method_chain_narrowings
|
|
700
|
+
@method_chain_narrowings == other.method_chain_narrowings &&
|
|
701
|
+
@declaration_sourced == other.declaration_sourced
|
|
568
702
|
end
|
|
569
703
|
alias eql? ==
|
|
570
704
|
|
|
@@ -580,7 +714,9 @@ module Rigor
|
|
|
580
714
|
discovery: @discovery,
|
|
581
715
|
indexed_narrowings: @indexed_narrowings,
|
|
582
716
|
method_chain_narrowings: @method_chain_narrowings,
|
|
583
|
-
|
|
717
|
+
declaration_sourced: @declaration_sourced,
|
|
718
|
+
source_path: @source_path,
|
|
719
|
+
struct_fold_safe_locals: @struct_fold_safe_locals
|
|
584
720
|
)
|
|
585
721
|
self.class.new(
|
|
586
722
|
environment: environment, locals: locals,
|
|
@@ -589,7 +725,9 @@ module Rigor
|
|
|
589
725
|
discovery: discovery,
|
|
590
726
|
indexed_narrowings: indexed_narrowings,
|
|
591
727
|
method_chain_narrowings: method_chain_narrowings,
|
|
592
|
-
|
|
728
|
+
declaration_sourced: declaration_sourced,
|
|
729
|
+
source_path: source_path,
|
|
730
|
+
struct_fold_safe_locals: struct_fold_safe_locals
|
|
593
731
|
)
|
|
594
732
|
end
|
|
595
733
|
|
|
@@ -621,10 +759,22 @@ module Rigor
|
|
|
621
759
|
discovery: @discovery,
|
|
622
760
|
indexed_narrowings: join_bindings(@indexed_narrowings, other.indexed_narrowings),
|
|
623
761
|
method_chain_narrowings: join_bindings(@method_chain_narrowings, other.method_chain_narrowings),
|
|
762
|
+
# ADR-58 WD1 — a ref is declaration-sourced after a join only when
|
|
763
|
+
# BOTH branches agree it is. If either path made the binding
|
|
764
|
+
# flow-live (a method-local nil write / failed-guard narrowing), the
|
|
765
|
+
# merge is flow-live and `possible-nil-receiver` fires as before.
|
|
766
|
+
declaration_sourced: join_declaration_sourced(other),
|
|
624
767
|
source_path: source_path
|
|
625
768
|
)
|
|
626
769
|
end
|
|
627
770
|
|
|
771
|
+
def join_declaration_sourced(other)
|
|
772
|
+
return @declaration_sourced if @declaration_sourced.equal?(other.declaration_sourced)
|
|
773
|
+
return EMPTY_DECLARATION_SOURCED if @declaration_sourced.empty? || other.declaration_sourced.empty?
|
|
774
|
+
|
|
775
|
+
(@declaration_sourced & other.declaration_sourced).freeze
|
|
776
|
+
end
|
|
777
|
+
|
|
628
778
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
629
779
|
IndexedKey.new(
|
|
630
780
|
receiver_kind: receiver_kind.to_sym,
|
|
@@ -652,6 +802,25 @@ module Rigor
|
|
|
652
802
|
filtered.size == @indexed_narrowings.size ? @indexed_narrowings : filtered.freeze
|
|
653
803
|
end
|
|
654
804
|
|
|
805
|
+
# ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
|
|
806
|
+
def add_declaration_sourced(kind, name)
|
|
807
|
+
ref = [kind.to_sym, name.to_sym]
|
|
808
|
+
return @declaration_sourced if @declaration_sourced.include?(ref)
|
|
809
|
+
|
|
810
|
+
(@declaration_sourced.dup << ref).freeze
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def drop_declaration_sourced_for(kind, name)
|
|
814
|
+
return @declaration_sourced if @declaration_sourced.empty?
|
|
815
|
+
|
|
816
|
+
ref = [kind.to_sym, name.to_sym]
|
|
817
|
+
return @declaration_sourced unless @declaration_sourced.include?(ref)
|
|
818
|
+
|
|
819
|
+
dropped = @declaration_sourced.dup
|
|
820
|
+
dropped.delete(ref)
|
|
821
|
+
dropped.freeze
|
|
822
|
+
end
|
|
823
|
+
|
|
655
824
|
def drop_chain_narrowings_for(receiver_kind, receiver_name)
|
|
656
825
|
return @method_chain_narrowings if @method_chain_narrowings.empty?
|
|
657
826
|
|
|
@@ -118,7 +118,15 @@ module Rigor
|
|
|
118
118
|
@class_shells = Set.new
|
|
119
119
|
defs = collect_method_definitions(parse_result.value)
|
|
120
120
|
candidates_from_defs = defs.filter_map do |def_node, class_name, kind|
|
|
121
|
+
# An analyzer bug typing one def's body must cost only that
|
|
122
|
+
# def's candidate, never the whole `rigor sig-gen` run. The
|
|
123
|
+
# `check` path recovers each *file* this way
|
|
124
|
+
# (worker_session.rb); sig-gen recovers per-def so the rest of
|
|
125
|
+
# the file's candidates still emit.
|
|
126
|
+
|
|
121
127
|
classify_def(path, def_node, class_name, kind, scope_index)
|
|
128
|
+
rescue StandardError
|
|
129
|
+
nil
|
|
122
130
|
end
|
|
123
131
|
obs_ivar_map = build_observed_ivar_map(parse_result.value)
|
|
124
132
|
candidates_from_defs + collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
|
|
@@ -5,9 +5,9 @@ module Rigor
|
|
|
5
5
|
# Per-call-site argument observation produced by
|
|
6
6
|
# {ObservationCollector}. ADR-14 follow-up: the earlier
|
|
7
7
|
# MVP shape (`Array[Type]` of positional types only)
|
|
8
|
-
# could not represent keyword arguments —
|
|
9
|
-
# `MethodCatalog.new(path: ..., mutating_selectors: ...)`
|
|
10
|
-
#
|
|
8
|
+
# could not represent keyword arguments — keyword calls
|
|
9
|
+
# like `MethodCatalog.new(path: ..., mutating_selectors: ...)`
|
|
10
|
+
# were silently skipped in that shape.
|
|
11
11
|
# The new shape carries positional and keyword arg types
|
|
12
12
|
# in parallel so the per-position / per-keyword unions
|
|
13
13
|
# can each be reconstructed independently.
|
data/lib/rigor/sig_gen/writer.rb
CHANGED
|
@@ -46,6 +46,11 @@ module Rigor
|
|
|
46
46
|
def initialize(path_mapper:, overwrite: false)
|
|
47
47
|
@path_mapper = path_mapper
|
|
48
48
|
@overwrite = overwrite
|
|
49
|
+
# Run-level (cross-file) namespace-kind view, populated
|
|
50
|
+
# per `#write_all` from every candidate's per-file map.
|
|
51
|
+
# Empty until then so the single-target `#write` path
|
|
52
|
+
# falls back to per-candidate kinds only.
|
|
53
|
+
@global_namespace_kinds = {}
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
# Process the full candidate list by resolving each
|
|
@@ -65,6 +70,7 @@ module Rigor
|
|
|
65
70
|
emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
|
|
66
71
|
return [] if emittable.empty?
|
|
67
72
|
|
|
73
|
+
@global_namespace_kinds = build_namespace_kinds(candidates)
|
|
68
74
|
emittable.group_by { |c| @path_mapper.target_for(c.path, class_name: c.class_name) }
|
|
69
75
|
.map { |target, group| write_target(target, group) }
|
|
70
76
|
end
|
|
@@ -161,13 +167,45 @@ module Rigor
|
|
|
161
167
|
end
|
|
162
168
|
|
|
163
169
|
def merged_namespace_kinds(candidates)
|
|
164
|
-
merged =
|
|
170
|
+
merged = @global_namespace_kinds.dup
|
|
165
171
|
candidates.each do |c|
|
|
166
|
-
(c.namespace_kinds || {}).each { |k, v| merged
|
|
172
|
+
(c.namespace_kinds || {}).each { |k, v| apply_namespace_kind(merged, k, v) }
|
|
167
173
|
end
|
|
168
174
|
merged
|
|
169
175
|
end
|
|
170
176
|
|
|
177
|
+
# Folds every candidate's per-file namespace-kind map
|
|
178
|
+
# into one run-level view so a `class Foo` recorded
|
|
179
|
+
# while scanning `foo.rb` governs the wrapper keyword
|
|
180
|
+
# emitted for `Foo` in a *sibling* file's target — e.g.
|
|
181
|
+
# `foo/bar.rb` declaring `class Foo::Bar`, whose compact
|
|
182
|
+
# constant path never names `Foo`, so the walker records
|
|
183
|
+
# no kind for it. Without this view that sibling target
|
|
184
|
+
# wraps the nested class in `module Foo` while `foo.rbs`
|
|
185
|
+
# declares `class Foo`; loading both raises
|
|
186
|
+
# `RBS::DuplicatedDeclarationError`, aborting the whole
|
|
187
|
+
# RBS env build.
|
|
188
|
+
def build_namespace_kinds(candidates)
|
|
189
|
+
candidates.each_with_object({}) do |candidate, acc|
|
|
190
|
+
(candidate.namespace_kinds || {}).each { |name, kind| apply_namespace_kind(acc, name, kind) }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# A `class` declaration is authoritative and MUST win
|
|
195
|
+
# over the `:module` wrapper default: a compact
|
|
196
|
+
# `class Foo::Bar` never names `Foo`, so the only signal
|
|
197
|
+
# for `Foo`'s kind is an actual `class Foo` (or
|
|
198
|
+
# `Const = Data.define(...)` shell) seen elsewhere. This
|
|
199
|
+
# guarantees the generated tree never mixes `class` /
|
|
200
|
+
# `module` for the same constant.
|
|
201
|
+
def apply_namespace_kind(map, key, kind)
|
|
202
|
+
if kind == :class
|
|
203
|
+
map[key] = :class
|
|
204
|
+
else
|
|
205
|
+
map[key] ||= :module
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
171
209
|
# Tree node: { name:, children: Hash{String => node},
|
|
172
210
|
# methods: Array<MethodCandidate>, shell: Boolean }.
|
|
173
211
|
# `shell` flags nodes that came in via `class_shells`
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Source
|
|
5
|
+
# Flattens a Prism constant-reference node (`ConstantReadNode` /
|
|
6
|
+
# `ConstantPathNode`) to its source-qualified `"A::B::C"` string.
|
|
7
|
+
#
|
|
8
|
+
# Two nil policies for the one edge case that distinguishes the call
|
|
9
|
+
# sites — a constant path rooted in a *dynamic* base (`expr::Bar`, where
|
|
10
|
+
# the left side is a runtime expression rather than a constant):
|
|
11
|
+
#
|
|
12
|
+
# * {.qualified_name} / {.render} are LENIENT — they drop the dynamic
|
|
13
|
+
# segment and render the trailing constant names (`expr::Bar` => "Bar").
|
|
14
|
+
# The scope indexer and statement evaluator feed only genuine
|
|
15
|
+
# class/module path nodes and want a best-effort name.
|
|
16
|
+
# * {.qualified_name_or_nil} is STRICT — a dynamic base anywhere in the
|
|
17
|
+
# chain yields `nil`, so a caller that statically names constants can
|
|
18
|
+
# treat the path as opaque rather than guessing.
|
|
19
|
+
#
|
|
20
|
+
# A leading `::` (absolute root, `::Foo`) renders as `"Foo"` under both
|
|
21
|
+
# policies. A node that is neither a `ConstantReadNode` nor a
|
|
22
|
+
# `ConstantPathNode` yields `nil` under both.
|
|
23
|
+
module ConstantPath
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Lenient dispatch over a constant-reference node.
|
|
27
|
+
def qualified_name(node)
|
|
28
|
+
case node
|
|
29
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
30
|
+
when Prism::ConstantPathNode then render(node)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Lenient render of a `ConstantPathNode`; never nil for a path node.
|
|
35
|
+
def render(node)
|
|
36
|
+
prefix =
|
|
37
|
+
case node.parent
|
|
38
|
+
when Prism::ConstantReadNode then "#{node.parent.name}::"
|
|
39
|
+
when Prism::ConstantPathNode then "#{render(node.parent)}::"
|
|
40
|
+
else ""
|
|
41
|
+
end
|
|
42
|
+
"#{prefix}#{node.name}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Strict dispatch: a dynamic base anywhere in the path yields nil.
|
|
46
|
+
def qualified_name_or_nil(node)
|
|
47
|
+
case node
|
|
48
|
+
when Prism::ConstantReadNode
|
|
49
|
+
node.name.to_s
|
|
50
|
+
when Prism::ConstantPathNode
|
|
51
|
+
parent = node.parent
|
|
52
|
+
return node.name.to_s if parent.nil?
|
|
53
|
+
|
|
54
|
+
parent_name = qualified_name_or_nil(parent)
|
|
55
|
+
return nil if parent_name.nil?
|
|
56
|
+
|
|
57
|
+
"#{parent_name}::#{node.name}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/rigor/source.rb
CHANGED
|
@@ -304,26 +304,11 @@ module Rigor
|
|
|
304
304
|
m && [m[1], m[2]]
|
|
305
305
|
end
|
|
306
306
|
|
|
307
|
-
# Normalises a message receiver token to a class name.
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
# `singleton(Foo)` and bare `Foo` fold to `Foo`.
|
|
307
|
+
# Normalises a message receiver token to a class name. The fold
|
|
308
|
+
# logic is shared with the selector axis — see
|
|
309
|
+
# {Triage.normalize_receiver}.
|
|
311
310
|
def receiver_class(token)
|
|
312
|
-
|
|
313
|
-
return "Integer" if t.match?(/\A-?\d+\z/)
|
|
314
|
-
return "Float" if t.match?(/\A-?\d+\.\d+\z/)
|
|
315
|
-
return "String" if t.start_with?('"', "'")
|
|
316
|
-
return "Symbol" if t.start_with?(":")
|
|
317
|
-
|
|
318
|
-
singleton = t[/\Asingleton\(([\w:]+)\)\z/, 1]
|
|
319
|
-
return singleton if singleton
|
|
320
|
-
return t if t.start_with?("Array[")
|
|
321
|
-
|
|
322
|
-
nominal = t[/\A([\w:]+)\[/, 1]
|
|
323
|
-
return nominal if nominal
|
|
324
|
-
return t if t.match?(/\A[\w:]+\z/)
|
|
325
|
-
|
|
326
|
-
nil
|
|
311
|
+
Triage.normalize_receiver(token)
|
|
327
312
|
end
|
|
328
313
|
|
|
329
314
|
def activesupport?(receiver, method)
|
data/lib/rigor/triage.rb
CHANGED
|
@@ -18,7 +18,15 @@ module Rigor
|
|
|
18
18
|
Summary = Data.define(:total, :error, :warning, :info)
|
|
19
19
|
RuleCount = Data.define(:rule, :count)
|
|
20
20
|
Hotspot = Data.define(:file, :count, :by_rule)
|
|
21
|
-
|
|
21
|
+
# A (receiver-class, method) dispatch target the diagnostics
|
|
22
|
+
# cluster on, built from the structured `Diagnostic#receiver_type`
|
|
23
|
+
# / `#method_name` fields — never from message-string parsing.
|
|
24
|
+
# `receiver` is nil for method-only diagnostics (a toplevel call,
|
|
25
|
+
# a `def`-side return / override finding that has no call
|
|
26
|
+
# receiver); `files` is the distinct-file count (a systemic vs.
|
|
27
|
+
# localised signal); `rules` is the per-rule breakdown.
|
|
28
|
+
Selector = Data.define(:receiver, :method_name, :count, :files, :rules)
|
|
29
|
+
Report = Data.define(:summary, :distribution, :selectors, :hotspots, :hints)
|
|
22
30
|
|
|
23
31
|
module_function
|
|
24
32
|
|
|
@@ -30,6 +38,7 @@ module Rigor
|
|
|
30
38
|
Report.new(
|
|
31
39
|
summary: build_summary(diagnostics),
|
|
32
40
|
distribution: build_distribution(diagnostics),
|
|
41
|
+
selectors: build_selectors(diagnostics),
|
|
33
42
|
hotspots: build_hotspots(diagnostics, top),
|
|
34
43
|
hints: hints ? Catalogue.recognise(diagnostics) : []
|
|
35
44
|
)
|
|
@@ -57,6 +66,61 @@ module Rigor
|
|
|
57
66
|
.sort_by { |row| [-row.count, row.rule] }
|
|
58
67
|
end
|
|
59
68
|
|
|
69
|
+
# The class/method aggregation axis (ADR-23 follow-up). Groups
|
|
70
|
+
# every diagnostic that carries a `method_name` by its
|
|
71
|
+
# `(receiver_type, method_name)` pair so a consumer can answer
|
|
72
|
+
# "which method / class concentrates the diagnostics?" with a
|
|
73
|
+
# `jq` query over the JSON instead of parsing message text.
|
|
74
|
+
# Method-only diagnostics (nil `receiver_type`) keep a null
|
|
75
|
+
# `receiver` and still group by method. The full list is
|
|
76
|
+
# returned uncapped — the JSON is the agent-facing surface; the
|
|
77
|
+
# text renderer caps its own rows.
|
|
78
|
+
def build_selectors(diagnostics)
|
|
79
|
+
diagnostics.select(&:method_name)
|
|
80
|
+
.group_by { |d| [normalize_receiver(d.receiver_type) || d.receiver_type, d.method_name.to_s] }
|
|
81
|
+
.map { |(receiver, method), group| selector_for(receiver, method, group) }
|
|
82
|
+
.sort_by { |s| [-s.count, s.receiver.to_s, s.method_name] }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Folds a receiver token — a `Diagnostic#receiver_type` display
|
|
86
|
+
# string or a message-parsed token — to the class the diagnostics
|
|
87
|
+
# should bucket under, so the selector axis does not fragment one
|
|
88
|
+
# method across every distinct literal receiver. String / integer
|
|
89
|
+
# / float / symbol literals collapse to their class; `singleton(C)`
|
|
90
|
+
# and a bare `C` fold to `C`; a generic `C[...]` keeps the
|
|
91
|
+
# `Array[String]` element form (the AR-relation heuristic keys on
|
|
92
|
+
# it). Returns nil for a token it cannot reduce to a class (a
|
|
93
|
+
# union display, an inferred shape) — the caller keeps the raw
|
|
94
|
+
# string then, never losing the row. Shared with {Catalogue}.
|
|
95
|
+
def normalize_receiver(token)
|
|
96
|
+
return nil if token.nil?
|
|
97
|
+
|
|
98
|
+
t = token.to_s.strip
|
|
99
|
+
return "Integer" if t.match?(/\A-?\d+\z/)
|
|
100
|
+
return "Float" if t.match?(/\A-?\d+\.\d+\z/)
|
|
101
|
+
return "String" if t.start_with?('"', "'")
|
|
102
|
+
return "Symbol" if t.start_with?(":")
|
|
103
|
+
|
|
104
|
+
singleton = t[/\Asingleton\(([\w:]+)\)\z/, 1]
|
|
105
|
+
return singleton if singleton
|
|
106
|
+
return t if t.start_with?("Array[")
|
|
107
|
+
|
|
108
|
+
nominal = t[/\A([\w:]+)\[/, 1]
|
|
109
|
+
return nominal if nominal
|
|
110
|
+
return t if t.match?(/\A[\w:]+\z/)
|
|
111
|
+
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def selector_for(receiver, method, group)
|
|
116
|
+
rules = group.group_by { |d| rule_key(d) }
|
|
117
|
+
.transform_values(&:size)
|
|
118
|
+
.sort_by { |rule, count| [-count, rule] }
|
|
119
|
+
.to_h
|
|
120
|
+
Selector.new(receiver: receiver, method_name: method, count: group.size,
|
|
121
|
+
files: group.map(&:path).uniq.size, rules: rules)
|
|
122
|
+
end
|
|
123
|
+
|
|
60
124
|
def build_hotspots(diagnostics, top)
|
|
61
125
|
diagnostics.group_by(&:path)
|
|
62
126
|
.map { |path, group| hotspot_for(path, group) }
|
|
@@ -79,6 +143,10 @@ module Rigor
|
|
|
79
143
|
"warning" => report.summary.warning, "info" => report.summary.info
|
|
80
144
|
},
|
|
81
145
|
"distribution" => report.distribution.map { |r| { "rule" => r.rule, "count" => r.count } },
|
|
146
|
+
"selectors" => report.selectors.map do |s|
|
|
147
|
+
{ "receiver" => s.receiver, "method" => s.method_name, "count" => s.count,
|
|
148
|
+
"files" => s.files, "rules" => s.rules }
|
|
149
|
+
end,
|
|
82
150
|
"hotspots" => report.hotspots.map do |h|
|
|
83
151
|
{ "file" => h.file, "count" => h.count, "by_rule" => h.by_rule }
|
|
84
152
|
end,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../trinary"
|
|
4
4
|
require_relative "../value_semantics"
|
|
5
5
|
require_relative "acceptance_router"
|
|
6
|
+
require_relative "plain_lattice"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Type
|
|
@@ -46,17 +47,7 @@ module Rigor
|
|
|
46
47
|
"Method"
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
Trinary.no
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def bot
|
|
54
|
-
Trinary.no
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def dynamic
|
|
58
|
-
Trinary.no
|
|
59
|
-
end
|
|
50
|
+
include Rigor::Type::PlainLattice
|
|
60
51
|
|
|
61
52
|
include Rigor::Type::AcceptanceRouter
|
|
62
53
|
|