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
|
@@ -6,9 +6,12 @@ require_relative "../reflection"
|
|
|
6
6
|
require_relative "../source/node_walker"
|
|
7
7
|
require_relative "../type"
|
|
8
8
|
require_relative "diagnostic"
|
|
9
|
+
require_relative "dependency_recorder"
|
|
9
10
|
require_relative "check_rules/always_truthy_condition_collector"
|
|
11
|
+
require_relative "check_rules/unreachable_clause_collector"
|
|
10
12
|
require_relative "check_rules/dead_assignment_collector"
|
|
11
13
|
require_relative "check_rules/ivar_write_collector"
|
|
14
|
+
require_relative "check_rules/self_closedness_scanner"
|
|
12
15
|
|
|
13
16
|
module Rigor
|
|
14
17
|
module Analysis
|
|
@@ -57,6 +60,7 @@ module Rigor
|
|
|
57
60
|
# system; new rules MUST register here so user configuration
|
|
58
61
|
# can refer to them.
|
|
59
62
|
RULE_UNDEFINED_METHOD = "call.undefined-method"
|
|
63
|
+
RULE_SELF_UNDEFINED_METHOD = "call.self-undefined-method"
|
|
60
64
|
RULE_UNRESOLVED_TOPLEVEL = "call.unresolved-toplevel"
|
|
61
65
|
RULE_WRONG_ARITY = "call.wrong-arity"
|
|
62
66
|
RULE_ARGUMENT_TYPE = "call.argument-type-mismatch"
|
|
@@ -73,9 +77,11 @@ module Rigor
|
|
|
73
77
|
RULE_IVAR_WRITE_MISMATCH = "def.ivar-write-mismatch"
|
|
74
78
|
RULE_DEAD_ASSIGNMENT = "flow.dead-assignment"
|
|
75
79
|
RULE_ALWAYS_TRUTHY_CONDITION = "flow.always-truthy-condition"
|
|
80
|
+
RULE_UNREACHABLE_CLAUSE = "flow.unreachable-clause"
|
|
76
81
|
|
|
77
82
|
ALL_RULES = [
|
|
78
83
|
RULE_UNDEFINED_METHOD,
|
|
84
|
+
RULE_SELF_UNDEFINED_METHOD,
|
|
79
85
|
RULE_UNRESOLVED_TOPLEVEL,
|
|
80
86
|
RULE_WRONG_ARITY,
|
|
81
87
|
RULE_ARGUMENT_TYPE,
|
|
@@ -86,6 +92,7 @@ module Rigor
|
|
|
86
92
|
RULE_UNREACHABLE_BRANCH,
|
|
87
93
|
RULE_DEAD_ASSIGNMENT,
|
|
88
94
|
RULE_ALWAYS_TRUTHY_CONDITION,
|
|
95
|
+
RULE_UNREACHABLE_CLAUSE,
|
|
89
96
|
RULE_RETURN_TYPE,
|
|
90
97
|
RULE_VISIBILITY_MISMATCH,
|
|
91
98
|
RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
@@ -104,6 +111,7 @@ module Rigor
|
|
|
104
111
|
# both spellings resolve identically.
|
|
105
112
|
LEGACY_RULE_ALIASES = {
|
|
106
113
|
"undefined-method" => RULE_UNDEFINED_METHOD,
|
|
114
|
+
"self-undefined-method" => RULE_SELF_UNDEFINED_METHOD,
|
|
107
115
|
"wrong-arity" => RULE_WRONG_ARITY,
|
|
108
116
|
"argument-type-mismatch" => RULE_ARGUMENT_TYPE,
|
|
109
117
|
"possible-nil-receiver" => RULE_NIL_RECEIVER,
|
|
@@ -114,7 +122,8 @@ module Rigor
|
|
|
114
122
|
"method-visibility-mismatch" => RULE_VISIBILITY_MISMATCH,
|
|
115
123
|
"ivar-write-mismatch" => RULE_IVAR_WRITE_MISMATCH,
|
|
116
124
|
"dead-assignment" => RULE_DEAD_ASSIGNMENT,
|
|
117
|
-
"always-truthy-condition" => RULE_ALWAYS_TRUTHY_CONDITION
|
|
125
|
+
"always-truthy-condition" => RULE_ALWAYS_TRUTHY_CONDITION,
|
|
126
|
+
"unreachable-clause" => RULE_UNREACHABLE_CLAUSE
|
|
118
127
|
}.freeze
|
|
119
128
|
|
|
120
129
|
# Family wildcard — a `<family>` token in a suppression
|
|
@@ -153,7 +162,7 @@ module Rigor
|
|
|
153
162
|
# @param root [Prism::Node]
|
|
154
163
|
# @param scope_index [Hash{Prism::Node => Rigor::Scope}]
|
|
155
164
|
# @return [Array<Rigor::Analysis::Diagnostic>]
|
|
156
|
-
def diagnose(path:, root:, scope_index:, comments: [], disabled_rules: [])
|
|
165
|
+
def diagnose(path:, root:, scope_index:, self_call_misses: [], comments: [], disabled_rules: [])
|
|
157
166
|
diagnostics = []
|
|
158
167
|
Source::NodeWalker.each(root) do |node|
|
|
159
168
|
case node
|
|
@@ -173,7 +182,9 @@ module Rigor
|
|
|
173
182
|
diagnostics << unreachable if unreachable
|
|
174
183
|
end
|
|
175
184
|
end
|
|
185
|
+
diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index))
|
|
176
186
|
diagnostics.concat(always_truthy_condition_diagnostics(path, root, scope_index))
|
|
187
|
+
diagnostics.concat(unreachable_clause_diagnostics(path, root, scope_index))
|
|
177
188
|
diagnostics.concat(ivar_write_mismatch_diagnostics(path, root, scope_index))
|
|
178
189
|
diagnostics.concat(dead_assignment_diagnostics(path, root, scope_index))
|
|
179
190
|
filter_suppressed(diagnostics, comments: comments, disabled_rules: disabled_rules)
|
|
@@ -246,6 +257,16 @@ module Rigor
|
|
|
246
257
|
end
|
|
247
258
|
end
|
|
248
259
|
|
|
260
|
+
# ADR-47 — `flow.unreachable-clause`. One diagnostic per `when` clause
|
|
261
|
+
# the flow engine's narrowing proves can never match (its narrowed
|
|
262
|
+
# subject is `bot`). The squiggle lands on the dead clause's body,
|
|
263
|
+
# mirroring `flow.unreachable-branch`.
|
|
264
|
+
def unreachable_clause_diagnostics(path, root, scope_index)
|
|
265
|
+
UnreachableClauseCollector.new(scope_index).collect(root).map do |result|
|
|
266
|
+
build_unreachable_clause_diagnostic(path, result)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
249
270
|
def ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes)
|
|
250
271
|
return [] if writes.size < 2
|
|
251
272
|
|
|
@@ -383,7 +404,16 @@ module Rigor
|
|
|
383
404
|
# its model). Flagging an undefined method on a class
|
|
384
405
|
# with an open dynamic surface is unsound, so the rule
|
|
385
406
|
# skips it.
|
|
386
|
-
|
|
407
|
+
# An unbounded receiver surface: either a plugin-declared
|
|
408
|
+
# open receiver (ADR-26 — e.g. `ActiveRecord::Relation`), or
|
|
409
|
+
# a type Rigor synthesized (a missing-namespace module / a
|
|
410
|
+
# stub for a referenced-but-undeclared type) to keep a
|
|
411
|
+
# malformed project signature buildable. A synthesized stub's
|
|
412
|
+
# method table is empty only because Rigor invented it, not
|
|
413
|
+
# because the real type is empty (the real `DRb` has
|
|
414
|
+
# `start_service`), so enumerating it to prove a call
|
|
415
|
+
# "undefined" would be a false positive.
|
|
416
|
+
return nil if unbounded_receiver_surface?(class_name, scope)
|
|
387
417
|
|
|
388
418
|
# Slice 7 phase 12 — suppress when the user has
|
|
389
419
|
# declared the method in source (`def` /
|
|
@@ -518,11 +548,9 @@ module Rigor
|
|
|
518
548
|
end
|
|
519
549
|
|
|
520
550
|
def build_unresolved_toplevel_diagnostic(path, call_node)
|
|
521
|
-
|
|
522
|
-
|
|
551
|
+
Diagnostic.from_message_loc(
|
|
552
|
+
call_node,
|
|
523
553
|
path: path,
|
|
524
|
-
line: location.start_line,
|
|
525
|
-
column: location.start_column + 1,
|
|
526
554
|
message: "unresolved toplevel call to `#{call_node.name}`. " \
|
|
527
555
|
"If a project file defines `#{call_node.name}` via a toplevel " \
|
|
528
556
|
"`def` or a monkey-patch on Object/Kernel, list that file in " \
|
|
@@ -566,6 +594,14 @@ module Rigor
|
|
|
566
594
|
# loaded plugin (manifest `open_receivers:`). An open
|
|
567
595
|
# class responds beyond its RBS surface, so the
|
|
568
596
|
# `call.undefined-method` rule must not fire for it.
|
|
597
|
+
# True when the receiver class responds beyond an enumerable
|
|
598
|
+
# RBS method table, so proving a call "undefined" against it is
|
|
599
|
+
# unsound: a plugin-declared open receiver, or a Rigor-
|
|
600
|
+
# synthesized stub type (see `RbsLoader#synthesized_type_names`).
|
|
601
|
+
def unbounded_receiver_surface?(class_name, scope)
|
|
602
|
+
open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope)
|
|
603
|
+
end
|
|
604
|
+
|
|
569
605
|
def open_receiver?(class_name, scope)
|
|
570
606
|
registry = scope.environment&.plugin_registry
|
|
571
607
|
return false if registry.nil?
|
|
@@ -573,6 +609,13 @@ module Rigor
|
|
|
573
609
|
registry.open_receiver?(class_name)
|
|
574
610
|
end
|
|
575
611
|
|
|
612
|
+
def synthesized_stub_receiver?(class_name, scope)
|
|
613
|
+
loader = scope.environment&.rbs_loader
|
|
614
|
+
return false if loader.nil? || !loader.respond_to?(:synthesized_type_names)
|
|
615
|
+
|
|
616
|
+
loader.synthesized_type_names.include?(class_name.to_s.sub(/\A::/, ""))
|
|
617
|
+
end
|
|
618
|
+
|
|
576
619
|
def definition_available?(receiver_type, class_name, scope)
|
|
577
620
|
if receiver_type.is_a?(Type::Singleton)
|
|
578
621
|
!Rigor::Reflection.singleton_definition(class_name, scope: scope).nil?
|
|
@@ -581,6 +624,65 @@ module Rigor
|
|
|
581
624
|
end
|
|
582
625
|
end
|
|
583
626
|
|
|
627
|
+
# ADR-24 slice 4 — `call.self-undefined-method`. Consumes the engine's
|
|
628
|
+
# recorded unresolved implicit-self calls
|
|
629
|
+
# ({Analysis::SelfCallResolutionRecorder}) and adds only the
|
|
630
|
+
# closedness POLICY — it NEVER recomputes resolution (the reverted
|
|
631
|
+
# attempt-1 mistake that produced 135 FPs). A miss reaches here only
|
|
632
|
+
# because the engine's real resolution found the method nowhere.
|
|
633
|
+
#
|
|
634
|
+
# The v1 gate is deliberately the most conservative "confidently
|
|
635
|
+
# closed" shape: a STANDALONE project class — no superclass and no
|
|
636
|
+
# `include`/`prepend` (so its in-file method surface is complete) —
|
|
637
|
+
# that is not a module / mixin contract, defines no `method_missing`,
|
|
638
|
+
# has no dynamic `attr_*(*splat)` accessor, and is not an ADR-26 open
|
|
639
|
+
# receiver. Widening to superclass / include chains is a later slice,
|
|
640
|
+
# each behind the external corpus FP gate. Authored `:warning` but
|
|
641
|
+
# mapped to `:off` in every shipped profile until that gate is green
|
|
642
|
+
# (ADR-24 § "Slice 4"); a project opts in via `severity_overrides:`.
|
|
643
|
+
def self_undefined_method_diagnostics(path, self_call_misses, root, scope_index)
|
|
644
|
+
return [] if self_call_misses.empty?
|
|
645
|
+
|
|
646
|
+
open_names = SelfClosednessScanner.new(root).open_class_names
|
|
647
|
+
self_call_misses.filter_map do |miss|
|
|
648
|
+
next if open_names.include?(miss.class_name)
|
|
649
|
+
|
|
650
|
+
scope = scope_index[miss.node]
|
|
651
|
+
next if scope.nil?
|
|
652
|
+
next unless confidently_closed_self_class?(miss.class_name, scope)
|
|
653
|
+
|
|
654
|
+
build_self_undefined_method_diagnostic(path, miss)
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def confidently_closed_self_class?(class_name, scope)
|
|
659
|
+
return false if unbounded_receiver_surface?(class_name, scope)
|
|
660
|
+
return false if scope.discovered_method?(class_name, :method_missing, :instance)
|
|
661
|
+
# A superclass or mixin extends the surface beyond what this file
|
|
662
|
+
# declares; the engine's ancestor walk may have hit an unresolvable
|
|
663
|
+
# ancestor, so a miss is not provably a typo. Defer to a later slice.
|
|
664
|
+
return false if scope.superclass_of(class_name)
|
|
665
|
+
return false unless scope.includes_of(class_name).empty?
|
|
666
|
+
|
|
667
|
+
true
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def build_self_undefined_method_diagnostic(path, miss)
|
|
671
|
+
Diagnostic.new(
|
|
672
|
+
path: path,
|
|
673
|
+
line: miss.line || 1,
|
|
674
|
+
column: miss.column || 1,
|
|
675
|
+
message: "implicit-self call to `#{miss.method_name}` resolves to no method on " \
|
|
676
|
+
"`#{miss.class_name}` (a standalone class with a complete, project-known " \
|
|
677
|
+
"method surface). Likely a typo or a missing `def`.",
|
|
678
|
+
severity: :warning,
|
|
679
|
+
rule: RULE_SELF_UNDEFINED_METHOD,
|
|
680
|
+
source_family: :builtin,
|
|
681
|
+
receiver_type: miss.class_name,
|
|
682
|
+
method_name: miss.method_name
|
|
683
|
+
)
|
|
684
|
+
end
|
|
685
|
+
|
|
584
686
|
def lookup_method(receiver_type, class_name, method_name, scope)
|
|
585
687
|
if receiver_type.is_a?(Type::Singleton)
|
|
586
688
|
Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
|
|
@@ -808,11 +910,9 @@ module Rigor
|
|
|
808
910
|
return nil if inside_rigor_testing?(scope)
|
|
809
911
|
|
|
810
912
|
type = scope.type_of(arg)
|
|
811
|
-
|
|
812
|
-
|
|
913
|
+
Diagnostic.from_message_loc(
|
|
914
|
+
call_node,
|
|
813
915
|
path: path,
|
|
814
|
-
line: location.start_line,
|
|
815
|
-
column: location.start_column + 1,
|
|
816
916
|
message: "dump_type: #{type.describe(:short)}",
|
|
817
917
|
severity: :info,
|
|
818
918
|
rule: RULE_DUMP_TYPE
|
|
@@ -905,24 +1005,20 @@ module Rigor
|
|
|
905
1005
|
end
|
|
906
1006
|
|
|
907
1007
|
def build_assert_type_diagnostic(path, call_node, expected, actual)
|
|
908
|
-
|
|
909
|
-
|
|
1008
|
+
Diagnostic.from_message_loc(
|
|
1009
|
+
call_node,
|
|
910
1010
|
rule: RULE_ASSERT_TYPE,
|
|
911
1011
|
path: path,
|
|
912
|
-
line: location.start_line,
|
|
913
|
-
column: location.start_column + 1,
|
|
914
1012
|
message: "assert_type mismatch: expected #{expected.inspect}, got #{actual.inspect}",
|
|
915
1013
|
severity: :error
|
|
916
1014
|
)
|
|
917
1015
|
end
|
|
918
1016
|
|
|
919
1017
|
def build_nil_receiver_diagnostic(path, call_node)
|
|
920
|
-
|
|
921
|
-
|
|
1018
|
+
Diagnostic.from_message_loc(
|
|
1019
|
+
call_node,
|
|
922
1020
|
rule: RULE_NIL_RECEIVER,
|
|
923
1021
|
path: path,
|
|
924
|
-
line: location.start_line,
|
|
925
|
-
column: location.start_column + 1,
|
|
926
1022
|
message: "possible nil receiver: `#{call_node.name}' is undefined on NilClass",
|
|
927
1023
|
severity: :error
|
|
928
1024
|
)
|
|
@@ -985,12 +1081,10 @@ module Rigor
|
|
|
985
1081
|
end
|
|
986
1082
|
|
|
987
1083
|
def build_always_raises_diagnostic(path, call_node)
|
|
988
|
-
|
|
989
|
-
|
|
1084
|
+
Diagnostic.from_message_loc(
|
|
1085
|
+
call_node,
|
|
990
1086
|
rule: RULE_ALWAYS_RAISES,
|
|
991
1087
|
path: path,
|
|
992
|
-
line: location.start_line,
|
|
993
|
-
column: location.start_column + 1,
|
|
994
1088
|
message: "always raises ZeroDivisionError: `#{call_node.name}' by zero on Integer receiver",
|
|
995
1089
|
severity: :error
|
|
996
1090
|
)
|
|
@@ -1109,12 +1203,10 @@ module Rigor
|
|
|
1109
1203
|
end
|
|
1110
1204
|
|
|
1111
1205
|
def build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
|
|
1112
|
-
|
|
1113
|
-
|
|
1206
|
+
Diagnostic.from_message_loc(
|
|
1207
|
+
call_node,
|
|
1114
1208
|
rule: RULE_VISIBILITY_MISMATCH,
|
|
1115
1209
|
path: path,
|
|
1116
|
-
line: location.start_line,
|
|
1117
|
-
column: location.start_column + 1,
|
|
1118
1210
|
message: "private method `#{call_node.name}' called on #{receiver_type.class_name} receiver",
|
|
1119
1211
|
severity: :error
|
|
1120
1212
|
)
|
|
@@ -1142,36 +1234,55 @@ module Rigor
|
|
|
1142
1234
|
end
|
|
1143
1235
|
|
|
1144
1236
|
def build_always_truthy_condition_diagnostic(path, predicate_node, polarity)
|
|
1145
|
-
|
|
1146
|
-
|
|
1237
|
+
Diagnostic.from_node(
|
|
1238
|
+
predicate_node,
|
|
1147
1239
|
rule: RULE_ALWAYS_TRUTHY_CONDITION,
|
|
1148
1240
|
path: path,
|
|
1149
|
-
line: location.start_line,
|
|
1150
|
-
column: location.start_column + 1,
|
|
1151
1241
|
message: "condition is always #{polarity} (the surrounding flow proves it folds to a constant)",
|
|
1152
1242
|
severity: :warning
|
|
1153
1243
|
)
|
|
1154
1244
|
end
|
|
1155
1245
|
|
|
1246
|
+
def build_unreachable_clause_diagnostic(path, result)
|
|
1247
|
+
Diagnostic.from_node(
|
|
1248
|
+
result.body,
|
|
1249
|
+
rule: RULE_UNREACHABLE_CLAUSE,
|
|
1250
|
+
path: path,
|
|
1251
|
+
message: unreachable_clause_message(result),
|
|
1252
|
+
severity: :warning
|
|
1253
|
+
)
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
def unreachable_clause_message(result)
|
|
1257
|
+
subject = result.subject_name
|
|
1258
|
+
kw = result.keyword
|
|
1259
|
+
case result.kind
|
|
1260
|
+
when :prior_exhaustion
|
|
1261
|
+
"unreachable `#{kw} #{result.condition_source}': `#{subject}' is already covered " \
|
|
1262
|
+
"by an earlier `#{kw}' clause"
|
|
1263
|
+
when :exhausted_else
|
|
1264
|
+
"unreachable `else': the `#{kw}' clauses already cover every value `#{subject}' can take here"
|
|
1265
|
+
else # :disjoint
|
|
1266
|
+
"unreachable `#{kw} #{result.condition_source}': `#{subject}' can never be " \
|
|
1267
|
+
"#{result.condition_source} here (the flow proves the subject disjoint)"
|
|
1268
|
+
end
|
|
1269
|
+
end
|
|
1270
|
+
|
|
1156
1271
|
def build_dead_assignment_diagnostic(path, write_node, def_node)
|
|
1157
|
-
|
|
1158
|
-
|
|
1272
|
+
Diagnostic.from_name_loc(
|
|
1273
|
+
write_node,
|
|
1159
1274
|
rule: RULE_DEAD_ASSIGNMENT,
|
|
1160
1275
|
path: path,
|
|
1161
|
-
line: location.start_line,
|
|
1162
|
-
column: location.start_column + 1,
|
|
1163
1276
|
message: "local `#{write_node.name}' assigned in `#{def_node.name}' but never read",
|
|
1164
1277
|
severity: :warning
|
|
1165
1278
|
)
|
|
1166
1279
|
end
|
|
1167
1280
|
|
|
1168
1281
|
def build_ivar_write_mismatch_diagnostic(path, node, class_name, ivar_name, first_class, other_class)
|
|
1169
|
-
|
|
1170
|
-
|
|
1282
|
+
Diagnostic.from_name_loc(
|
|
1283
|
+
node,
|
|
1171
1284
|
rule: RULE_IVAR_WRITE_MISMATCH,
|
|
1172
1285
|
path: path,
|
|
1173
|
-
line: location.start_line,
|
|
1174
|
-
column: location.start_column + 1,
|
|
1175
1286
|
message: "instance variable `#{ivar_name}' on #{class_name} was previously assigned " \
|
|
1176
1287
|
"#{first_class}; this write assigns #{other_class}",
|
|
1177
1288
|
severity: :error
|
|
@@ -1190,12 +1301,10 @@ module Rigor
|
|
|
1190
1301
|
end
|
|
1191
1302
|
|
|
1192
1303
|
def build_unreachable_branch_diagnostic(path, dead_branch, polarity)
|
|
1193
|
-
|
|
1194
|
-
|
|
1304
|
+
Diagnostic.from_node(
|
|
1305
|
+
dead_branch,
|
|
1195
1306
|
rule: RULE_UNREACHABLE_BRANCH,
|
|
1196
1307
|
path: path,
|
|
1197
|
-
line: location.start_line,
|
|
1198
|
-
column: location.start_column + 1,
|
|
1199
1308
|
message: "unreachable branch: literal predicate is always #{polarity}",
|
|
1200
1309
|
severity: :warning
|
|
1201
1310
|
)
|
|
@@ -1324,39 +1433,34 @@ module Rigor
|
|
|
1324
1433
|
end
|
|
1325
1434
|
|
|
1326
1435
|
def build_argument_type_diagnostic(path, call_node, class_name, mismatch)
|
|
1327
|
-
location = mismatch[:node].location
|
|
1328
1436
|
method_label = "`#{call_node.name}' on #{class_name}"
|
|
1329
1437
|
parameter_label = mismatch[:name] ? "parameter `#{mismatch[:name]}' of #{method_label}" : method_label
|
|
1330
1438
|
message = "argument type mismatch at #{parameter_label}: " \
|
|
1331
1439
|
"expected #{mismatch[:expected].describe(:short)}, " \
|
|
1332
1440
|
"got #{mismatch[:actual].describe(:short)}"
|
|
1333
|
-
Diagnostic.
|
|
1441
|
+
Diagnostic.from_node(
|
|
1442
|
+
mismatch[:node],
|
|
1334
1443
|
rule: RULE_ARGUMENT_TYPE,
|
|
1335
1444
|
path: path,
|
|
1336
|
-
line: location.start_line,
|
|
1337
|
-
column: location.start_column + 1,
|
|
1338
1445
|
message: message,
|
|
1339
1446
|
severity: :error
|
|
1340
1447
|
)
|
|
1341
1448
|
end
|
|
1342
1449
|
|
|
1343
1450
|
def build_arity_diagnostic(path, call_node, class_name, min, max, actual)
|
|
1344
|
-
location = call_node.message_loc || call_node.location
|
|
1345
1451
|
range = min == max ? min.to_s : "#{min}..#{max}"
|
|
1346
1452
|
method_label = "`#{call_node.name}' on #{class_name}"
|
|
1347
1453
|
message = "wrong number of arguments to #{method_label} (given #{actual}, expected #{range})"
|
|
1348
|
-
Diagnostic.
|
|
1454
|
+
Diagnostic.from_message_loc(
|
|
1455
|
+
call_node,
|
|
1349
1456
|
rule: RULE_WRONG_ARITY,
|
|
1350
1457
|
path: path,
|
|
1351
|
-
line: location.start_line,
|
|
1352
|
-
column: location.start_column + 1,
|
|
1353
1458
|
message: message,
|
|
1354
1459
|
severity: :error
|
|
1355
1460
|
)
|
|
1356
1461
|
end
|
|
1357
1462
|
|
|
1358
1463
|
def build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site = nil, class_name = nil)
|
|
1359
|
-
location = call_node.message_loc || call_node.location
|
|
1360
1464
|
rendered_receiver = receiver_type.describe
|
|
1361
1465
|
message = "undefined method `#{call_node.name}' for #{rendered_receiver}"
|
|
1362
1466
|
# ADR-17 — when the project itself defines this method on the
|
|
@@ -1372,11 +1476,10 @@ module Rigor
|
|
|
1372
1476
|
"#{definition_site} — Rigor does not apply project monkey-patches " \
|
|
1373
1477
|
"cross-file; list that file in `.rigor.yml`'s `pre_eval:` (ADR-17)"
|
|
1374
1478
|
end
|
|
1375
|
-
Diagnostic.
|
|
1479
|
+
Diagnostic.from_message_loc(
|
|
1480
|
+
call_node,
|
|
1376
1481
|
rule: RULE_UNDEFINED_METHOD,
|
|
1377
1482
|
path: path,
|
|
1378
|
-
line: location.start_line,
|
|
1379
|
-
column: location.start_column + 1,
|
|
1380
1483
|
message: message,
|
|
1381
1484
|
severity: :error,
|
|
1382
1485
|
receiver_type: rendered_receiver,
|
|
@@ -1520,12 +1623,10 @@ module Rigor
|
|
|
1520
1623
|
end
|
|
1521
1624
|
|
|
1522
1625
|
def build_return_type_mismatch_diagnostic(path, def_node, declared, inferred, severity)
|
|
1523
|
-
|
|
1524
|
-
|
|
1626
|
+
Diagnostic.from_name_loc(
|
|
1627
|
+
def_node,
|
|
1525
1628
|
rule: RULE_RETURN_TYPE,
|
|
1526
1629
|
path: path,
|
|
1527
|
-
line: location.start_line,
|
|
1528
|
-
column: location.start_column + 1,
|
|
1529
1630
|
message: "return-type mismatch on `#{def_node.name}': " \
|
|
1530
1631
|
"declared #{declared.describe(:short)}, inferred #{inferred.describe(:short)}",
|
|
1531
1632
|
severity: severity
|
|
@@ -1655,6 +1756,14 @@ module Rigor
|
|
|
1655
1756
|
candidate = (segments[0, i] + [raw_ancestor]).join("::")
|
|
1656
1757
|
return candidate if known_user_class?(scope, candidate)
|
|
1657
1758
|
end
|
|
1759
|
+
# ADR-46 slice 3 — the override checker reads the class graph
|
|
1760
|
+
# directly (not through the recorder's `Scope` choke points), and
|
|
1761
|
+
# short-circuits when the ancestor resolves to no project class, so
|
|
1762
|
+
# an incremental re-check has no edge telling it to re-check this
|
|
1763
|
+
# subclass when that ancestor is later defined. Record a negative
|
|
1764
|
+
# class edge (keyed on the unqualified name) so the appeared-class
|
|
1765
|
+
# widening picks it up.
|
|
1766
|
+
DependencyRecorder.read_missing(:class, raw_ancestor.to_s.split("::").last) if DependencyRecorder.active?
|
|
1658
1767
|
nil
|
|
1659
1768
|
end
|
|
1660
1769
|
|
|
@@ -1665,12 +1774,10 @@ module Rigor
|
|
|
1665
1774
|
end
|
|
1666
1775
|
|
|
1667
1776
|
def build_override_visibility_diagnostic(path, def_node, parent_class, parent_visibility, override_visibility)
|
|
1668
|
-
|
|
1669
|
-
|
|
1777
|
+
Diagnostic.from_name_loc(
|
|
1778
|
+
def_node,
|
|
1670
1779
|
rule: RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
1671
1780
|
path: path,
|
|
1672
|
-
line: location.start_line,
|
|
1673
|
-
column: location.start_column + 1,
|
|
1674
1781
|
message: "visibility of `#{def_node.name}' reduced from #{parent_visibility} to " \
|
|
1675
1782
|
"#{override_visibility} (overrides #{parent_class}##{def_node.name}); " \
|
|
1676
1783
|
"breaks substitutability",
|
|
@@ -1760,12 +1867,10 @@ module Rigor
|
|
|
1760
1867
|
end
|
|
1761
1868
|
|
|
1762
1869
|
def build_override_return_widened_diagnostic(path, def_node, parent_class, parent_return, override_return)
|
|
1763
|
-
|
|
1764
|
-
|
|
1870
|
+
Diagnostic.from_name_loc(
|
|
1871
|
+
def_node,
|
|
1765
1872
|
rule: RULE_OVERRIDE_RETURN_WIDENED,
|
|
1766
1873
|
path: path,
|
|
1767
|
-
line: location.start_line,
|
|
1768
|
-
column: location.start_column + 1,
|
|
1769
1874
|
message: "return type of `#{def_node.name}' widened from #{parent_return.describe(:short)} " \
|
|
1770
1875
|
"to #{override_return.describe(:short)} (overrides #{parent_class}##{def_node.name}); " \
|
|
1771
1876
|
"breaks substitutability",
|
|
@@ -1866,12 +1971,10 @@ module Rigor
|
|
|
1866
1971
|
end
|
|
1867
1972
|
|
|
1868
1973
|
def build_override_param_narrowed_diagnostic(path, def_node, parent_class, index, parent_param, override_param)
|
|
1869
|
-
|
|
1870
|
-
|
|
1974
|
+
Diagnostic.from_name_loc(
|
|
1975
|
+
def_node,
|
|
1871
1976
|
rule: RULE_OVERRIDE_PARAM_NARROWED,
|
|
1872
1977
|
path: path,
|
|
1873
|
-
line: location.start_line,
|
|
1874
|
-
column: location.start_column + 1,
|
|
1875
1978
|
message: "parameter #{index + 1} of `#{def_node.name}' narrowed from " \
|
|
1876
1979
|
"#{parent_param.describe(:short)} to #{override_param.describe(:short)} " \
|
|
1877
1980
|
"(overrides #{parent_class}##{def_node.name}); breaks substitutability",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# ADR-46 slice 1 — records, per analyzed file, which OTHER source
|
|
6
|
+
# files its analysis read declarations / method bodies from (the
|
|
7
|
+
# cross-file dependency edges), plus the cross-file lookups that
|
|
8
|
+
# resolved to nothing (negative edges — adding that symbol later must
|
|
9
|
+
# re-check the consumer).
|
|
10
|
+
#
|
|
11
|
+
# Thread-local and activated per `analyze_file` only when the runner
|
|
12
|
+
# opts in (`record_dependencies: true`); a normal run never activates
|
|
13
|
+
# it, so {active?} is a single nil-check and the instrumented `Scope`
|
|
14
|
+
# accessors pay nothing. Recording is purely observational — it never
|
|
15
|
+
# changes a diagnostic.
|
|
16
|
+
#
|
|
17
|
+
# Modelled on {Inference::BudgetTrace}: process-thread-local state, a
|
|
18
|
+
# cheap disabled fast path, and a frozen snapshot for consumers.
|
|
19
|
+
module DependencyRecorder
|
|
20
|
+
KEY = :__rigor_dependency_recorder__
|
|
21
|
+
private_constant :KEY
|
|
22
|
+
|
|
23
|
+
# Mutable per-consumer accumulator. Frozen into a {Record} snapshot
|
|
24
|
+
# when `record_for` returns.
|
|
25
|
+
class Accumulator
|
|
26
|
+
attr_reader :consumer, :sources, :missing, :symbol_sources, :ancestry_sources
|
|
27
|
+
|
|
28
|
+
def initialize(consumer)
|
|
29
|
+
@consumer = consumer
|
|
30
|
+
@sources = Set.new
|
|
31
|
+
@missing = Set.new
|
|
32
|
+
# ADR-46 slice 4 — symbol-granularity tracking.
|
|
33
|
+
# `symbol_sources`: source_path → Set<"ClassName#method"> for method-call deps.
|
|
34
|
+
# `ancestry_sources`: Set<source_path> for class-ancestry (superclass / include)
|
|
35
|
+
# deps — file-granularity by nature (a superclass edge touches the whole class).
|
|
36
|
+
@symbol_sources = Hash.new { |h, k| h[k] = Set.new }
|
|
37
|
+
@ancestry_sources = Set.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def snapshot
|
|
41
|
+
frozen_sym = @symbol_sources.transform_values(&:freeze).freeze
|
|
42
|
+
Record.new(
|
|
43
|
+
consumer: consumer,
|
|
44
|
+
sources: sources.dup.freeze,
|
|
45
|
+
missing: missing.dup.freeze,
|
|
46
|
+
symbol_sources: frozen_sym,
|
|
47
|
+
ancestry_sources: ancestry_sources.dup.freeze
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Frozen record of one file's cross-file reads.
|
|
53
|
+
# `symbol_sources`: source_path → frozen Set<"ClassName#method"> (method-call edges).
|
|
54
|
+
# `ancestry_sources`: frozen Set<source_path> (class-ancestry edges, file-granularity).
|
|
55
|
+
Record = Data.define(:consumer, :sources, :missing, :symbol_sources, :ancestry_sources)
|
|
56
|
+
|
|
57
|
+
# Module-level activation count so the disabled fast path
|
|
58
|
+
# ({active?}) is a plain integer read rather than a `Thread.current`
|
|
59
|
+
# hash lookup — `user_def_for` (the instrumented accessor) is on the
|
|
60
|
+
# per-dispatch hot path, so a normal (non-recording) run must pay as
|
|
61
|
+
# little as possible. The per-thread accumulator still isolates the
|
|
62
|
+
# actual recording, so a non-recording thread seeing `active?` true
|
|
63
|
+
# (another thread is recording) just performs an extra nil-check.
|
|
64
|
+
@active_count = 0
|
|
65
|
+
@mutex = Mutex.new
|
|
66
|
+
|
|
67
|
+
module_function
|
|
68
|
+
|
|
69
|
+
# Activates recording for `consumer` (the path being analyzed) for
|
|
70
|
+
# the duration of the block and returns the frozen {Record}. Nests
|
|
71
|
+
# safely (the inner consumer's reads do not leak to the outer one);
|
|
72
|
+
# restores the previous recorder on exit.
|
|
73
|
+
def record_for(consumer)
|
|
74
|
+
previous = Thread.current[KEY]
|
|
75
|
+
accumulator = Accumulator.new(consumer.to_s)
|
|
76
|
+
Thread.current[KEY] = accumulator
|
|
77
|
+
@mutex.synchronize { @active_count += 1 }
|
|
78
|
+
yield
|
|
79
|
+
accumulator.snapshot
|
|
80
|
+
ensure
|
|
81
|
+
Thread.current[KEY] = previous
|
|
82
|
+
@mutex.synchronize { @active_count -= 1 }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Plain integer read (GVL-atomic) — no `Thread.current` lookup on the
|
|
86
|
+
# disabled fast path.
|
|
87
|
+
def active?
|
|
88
|
+
@active_count.positive?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Records that the current consumer read a declaration / body whose
|
|
92
|
+
# definition site is `path_line` (a `"path:line"` String, or nil).
|
|
93
|
+
# When `symbol` is given (a `"ClassName#method"` String), the read is
|
|
94
|
+
# a method-call edge and is recorded at symbol granularity in
|
|
95
|
+
# `symbol_sources` in addition to the coarse `sources` set.
|
|
96
|
+
# Without `symbol` the read is a class-ancestry edge (file-granularity)
|
|
97
|
+
# and is added to `ancestry_sources` only.
|
|
98
|
+
# Self-reads and nil sites are ignored in all cases.
|
|
99
|
+
def read_site(path_line, symbol = nil)
|
|
100
|
+
accumulator = Thread.current[KEY]
|
|
101
|
+
return if accumulator.nil? || path_line.nil?
|
|
102
|
+
|
|
103
|
+
path = path_line.split(":", 2).first
|
|
104
|
+
return unless path && path != accumulator.consumer
|
|
105
|
+
|
|
106
|
+
accumulator.sources << path
|
|
107
|
+
if symbol
|
|
108
|
+
accumulator.symbol_sources[path] << symbol
|
|
109
|
+
else
|
|
110
|
+
accumulator.ancestry_sources << path
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Records a cross-file lookup of `name` (kind `:method` / `:class` /
|
|
115
|
+
# `:const` / …) that resolved to nothing — a negative dependency.
|
|
116
|
+
def read_missing(kind, name)
|
|
117
|
+
accumulator = Thread.current[KEY]
|
|
118
|
+
accumulator&.missing&.add("#{kind}:#{name}")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|