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
|
@@ -6,9 +6,13 @@ 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"
|
|
10
|
+
require_relative "check_rules/rule_walk"
|
|
9
11
|
require_relative "check_rules/always_truthy_condition_collector"
|
|
12
|
+
require_relative "check_rules/unreachable_clause_collector"
|
|
10
13
|
require_relative "check_rules/dead_assignment_collector"
|
|
11
14
|
require_relative "check_rules/ivar_write_collector"
|
|
15
|
+
require_relative "check_rules/self_closedness_scanner"
|
|
12
16
|
|
|
13
17
|
module Rigor
|
|
14
18
|
module Analysis
|
|
@@ -57,6 +61,7 @@ module Rigor
|
|
|
57
61
|
# system; new rules MUST register here so user configuration
|
|
58
62
|
# can refer to them.
|
|
59
63
|
RULE_UNDEFINED_METHOD = "call.undefined-method"
|
|
64
|
+
RULE_SELF_UNDEFINED_METHOD = "call.self-undefined-method"
|
|
60
65
|
RULE_UNRESOLVED_TOPLEVEL = "call.unresolved-toplevel"
|
|
61
66
|
RULE_WRONG_ARITY = "call.wrong-arity"
|
|
62
67
|
RULE_ARGUMENT_TYPE = "call.argument-type-mismatch"
|
|
@@ -73,9 +78,11 @@ module Rigor
|
|
|
73
78
|
RULE_IVAR_WRITE_MISMATCH = "def.ivar-write-mismatch"
|
|
74
79
|
RULE_DEAD_ASSIGNMENT = "flow.dead-assignment"
|
|
75
80
|
RULE_ALWAYS_TRUTHY_CONDITION = "flow.always-truthy-condition"
|
|
81
|
+
RULE_UNREACHABLE_CLAUSE = "flow.unreachable-clause"
|
|
76
82
|
|
|
77
83
|
ALL_RULES = [
|
|
78
84
|
RULE_UNDEFINED_METHOD,
|
|
85
|
+
RULE_SELF_UNDEFINED_METHOD,
|
|
79
86
|
RULE_UNRESOLVED_TOPLEVEL,
|
|
80
87
|
RULE_WRONG_ARITY,
|
|
81
88
|
RULE_ARGUMENT_TYPE,
|
|
@@ -86,6 +93,7 @@ module Rigor
|
|
|
86
93
|
RULE_UNREACHABLE_BRANCH,
|
|
87
94
|
RULE_DEAD_ASSIGNMENT,
|
|
88
95
|
RULE_ALWAYS_TRUTHY_CONDITION,
|
|
96
|
+
RULE_UNREACHABLE_CLAUSE,
|
|
89
97
|
RULE_RETURN_TYPE,
|
|
90
98
|
RULE_VISIBILITY_MISMATCH,
|
|
91
99
|
RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
@@ -104,6 +112,7 @@ module Rigor
|
|
|
104
112
|
# both spellings resolve identically.
|
|
105
113
|
LEGACY_RULE_ALIASES = {
|
|
106
114
|
"undefined-method" => RULE_UNDEFINED_METHOD,
|
|
115
|
+
"self-undefined-method" => RULE_SELF_UNDEFINED_METHOD,
|
|
107
116
|
"wrong-arity" => RULE_WRONG_ARITY,
|
|
108
117
|
"argument-type-mismatch" => RULE_ARGUMENT_TYPE,
|
|
109
118
|
"possible-nil-receiver" => RULE_NIL_RECEIVER,
|
|
@@ -114,7 +123,8 @@ module Rigor
|
|
|
114
123
|
"method-visibility-mismatch" => RULE_VISIBILITY_MISMATCH,
|
|
115
124
|
"ivar-write-mismatch" => RULE_IVAR_WRITE_MISMATCH,
|
|
116
125
|
"dead-assignment" => RULE_DEAD_ASSIGNMENT,
|
|
117
|
-
"always-truthy-condition" => RULE_ALWAYS_TRUTHY_CONDITION
|
|
126
|
+
"always-truthy-condition" => RULE_ALWAYS_TRUTHY_CONDITION,
|
|
127
|
+
"unreachable-clause" => RULE_UNREACHABLE_CLAUSE
|
|
118
128
|
}.freeze
|
|
119
129
|
|
|
120
130
|
# Family wildcard — a `<family>` token in a suppression
|
|
@@ -153,7 +163,7 @@ module Rigor
|
|
|
153
163
|
# @param root [Prism::Node]
|
|
154
164
|
# @param scope_index [Hash{Prism::Node => Rigor::Scope}]
|
|
155
165
|
# @return [Array<Rigor::Analysis::Diagnostic>]
|
|
156
|
-
def diagnose(path:, root:, scope_index:, comments: [], disabled_rules: [])
|
|
166
|
+
def diagnose(path:, root:, scope_index:, self_call_misses: [], comments: [], disabled_rules: [])
|
|
157
167
|
diagnostics = []
|
|
158
168
|
Source::NodeWalker.each(root) do |node|
|
|
159
169
|
case node
|
|
@@ -173,12 +183,41 @@ module Rigor
|
|
|
173
183
|
diagnostics << unreachable if unreachable
|
|
174
184
|
end
|
|
175
185
|
end
|
|
176
|
-
diagnostics.concat(
|
|
186
|
+
diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index))
|
|
187
|
+
always_truthy_results, unreachable_clause_results = flow_collector_results(root, scope_index)
|
|
188
|
+
diagnostics.concat(always_truthy_condition_diagnostics(path, always_truthy_results))
|
|
189
|
+
diagnostics.concat(unreachable_clause_diagnostics(path, unreachable_clause_results))
|
|
177
190
|
diagnostics.concat(ivar_write_mismatch_diagnostics(path, root, scope_index))
|
|
178
191
|
diagnostics.concat(dead_assignment_diagnostics(path, root, scope_index))
|
|
179
192
|
filter_suppressed(diagnostics, comments: comments, disabled_rules: disabled_rules)
|
|
180
193
|
end
|
|
181
194
|
|
|
195
|
+
# ADR-53 Track B (slice B2) — both flow collectors ride one
|
|
196
|
+
# {RuleWalk} traversal instead of walking the file once each.
|
|
197
|
+
# Under `RIGOR_SHADOW_RULE_WALK=1` the legacy per-collector walks
|
|
198
|
+
# also run as the oracle and any divergence aborts the run — the
|
|
199
|
+
# corpus-scale half of the equivalence harness (the curated half
|
|
200
|
+
# is `rule_walk_equivalence_spec`).
|
|
201
|
+
def flow_collector_results(root, scope_index)
|
|
202
|
+
always_truthy = AlwaysTruthyConditionCollector.new(scope_index)
|
|
203
|
+
unreachable_clauses = UnreachableClauseCollector.new(scope_index)
|
|
204
|
+
RuleWalk.run(root, [always_truthy, unreachable_clauses])
|
|
205
|
+
if ENV["RIGOR_SHADOW_RULE_WALK"]
|
|
206
|
+
shadow_verify_flow_collectors(root, scope_index, always_truthy.results, unreachable_clauses.results)
|
|
207
|
+
end
|
|
208
|
+
[always_truthy.results, unreachable_clauses.results]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def shadow_verify_flow_collectors(root, scope_index, always_truthy_results, unreachable_clause_results)
|
|
212
|
+
legacy_always = AlwaysTruthyConditionCollector.new(scope_index).collect(root)
|
|
213
|
+
legacy_clauses = UnreachableClauseCollector.new(scope_index).collect(root)
|
|
214
|
+
return if legacy_always == always_truthy_results && legacy_clauses == unreachable_clause_results
|
|
215
|
+
|
|
216
|
+
raise "RIGOR_SHADOW_RULE_WALK divergence: always-truthy legacy=#{legacy_always.size} " \
|
|
217
|
+
"walk=#{always_truthy_results.size}; unreachable-clause legacy=#{legacy_clauses.size} " \
|
|
218
|
+
"walk=#{unreachable_clause_results.size}"
|
|
219
|
+
end
|
|
220
|
+
|
|
182
221
|
def call_node_diagnostics(path, node, scope_index)
|
|
183
222
|
[
|
|
184
223
|
undefined_method_diagnostic(path, node, scope_index),
|
|
@@ -240,12 +279,22 @@ module Rigor
|
|
|
240
279
|
# predicate skip envelope (see
|
|
241
280
|
# `Analysis::CheckRules::AlwaysTruthyConditionCollector`
|
|
242
281
|
# for the full triage rationale).
|
|
243
|
-
def always_truthy_condition_diagnostics(path,
|
|
244
|
-
|
|
282
|
+
def always_truthy_condition_diagnostics(path, results)
|
|
283
|
+
results.map do |result|
|
|
245
284
|
build_always_truthy_condition_diagnostic(path, result.node, result.polarity)
|
|
246
285
|
end
|
|
247
286
|
end
|
|
248
287
|
|
|
288
|
+
# ADR-47 — `flow.unreachable-clause`. One diagnostic per `when` clause
|
|
289
|
+
# the flow engine's narrowing proves can never match (its narrowed
|
|
290
|
+
# subject is `bot`). The squiggle lands on the dead clause's body,
|
|
291
|
+
# mirroring `flow.unreachable-branch`.
|
|
292
|
+
def unreachable_clause_diagnostics(path, results)
|
|
293
|
+
results.map do |result|
|
|
294
|
+
build_unreachable_clause_diagnostic(path, result)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
249
298
|
def ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes)
|
|
250
299
|
return [] if writes.size < 2
|
|
251
300
|
|
|
@@ -527,11 +576,9 @@ module Rigor
|
|
|
527
576
|
end
|
|
528
577
|
|
|
529
578
|
def build_unresolved_toplevel_diagnostic(path, call_node)
|
|
530
|
-
|
|
531
|
-
|
|
579
|
+
Diagnostic.from_message_loc(
|
|
580
|
+
call_node,
|
|
532
581
|
path: path,
|
|
533
|
-
line: location.start_line,
|
|
534
|
-
column: location.start_column + 1,
|
|
535
582
|
message: "unresolved toplevel call to `#{call_node.name}`. " \
|
|
536
583
|
"If a project file defines `#{call_node.name}` via a toplevel " \
|
|
537
584
|
"`def` or a monkey-patch on Object/Kernel, list that file in " \
|
|
@@ -605,6 +652,65 @@ module Rigor
|
|
|
605
652
|
end
|
|
606
653
|
end
|
|
607
654
|
|
|
655
|
+
# ADR-24 slice 4 — `call.self-undefined-method`. Consumes the engine's
|
|
656
|
+
# recorded unresolved implicit-self calls
|
|
657
|
+
# ({Analysis::SelfCallResolutionRecorder}) and adds only the
|
|
658
|
+
# closedness POLICY — it NEVER recomputes resolution (the reverted
|
|
659
|
+
# attempt-1 mistake that produced 135 FPs). A miss reaches here only
|
|
660
|
+
# because the engine's real resolution found the method nowhere.
|
|
661
|
+
#
|
|
662
|
+
# The v1 gate is deliberately the most conservative "confidently
|
|
663
|
+
# closed" shape: a STANDALONE project class — no superclass and no
|
|
664
|
+
# `include`/`prepend` (so its in-file method surface is complete) —
|
|
665
|
+
# that is not a module / mixin contract, defines no `method_missing`,
|
|
666
|
+
# has no dynamic `attr_*(*splat)` accessor, and is not an ADR-26 open
|
|
667
|
+
# receiver. Widening to superclass / include chains is a later slice,
|
|
668
|
+
# each behind the external corpus FP gate. Authored `:warning` but
|
|
669
|
+
# mapped to `:off` in every shipped profile until that gate is green
|
|
670
|
+
# (ADR-24 § "Slice 4"); a project opts in via `severity_overrides:`.
|
|
671
|
+
def self_undefined_method_diagnostics(path, self_call_misses, root, scope_index)
|
|
672
|
+
return [] if self_call_misses.empty?
|
|
673
|
+
|
|
674
|
+
open_names = SelfClosednessScanner.new(root).open_class_names
|
|
675
|
+
self_call_misses.filter_map do |miss|
|
|
676
|
+
next if open_names.include?(miss.class_name)
|
|
677
|
+
|
|
678
|
+
scope = scope_index[miss.node]
|
|
679
|
+
next if scope.nil?
|
|
680
|
+
next unless confidently_closed_self_class?(miss.class_name, scope)
|
|
681
|
+
|
|
682
|
+
build_self_undefined_method_diagnostic(path, miss)
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def confidently_closed_self_class?(class_name, scope)
|
|
687
|
+
return false if unbounded_receiver_surface?(class_name, scope)
|
|
688
|
+
return false if scope.discovered_method?(class_name, :method_missing, :instance)
|
|
689
|
+
# A superclass or mixin extends the surface beyond what this file
|
|
690
|
+
# declares; the engine's ancestor walk may have hit an unresolvable
|
|
691
|
+
# ancestor, so a miss is not provably a typo. Defer to a later slice.
|
|
692
|
+
return false if scope.superclass_of(class_name)
|
|
693
|
+
return false unless scope.includes_of(class_name).empty?
|
|
694
|
+
|
|
695
|
+
true
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def build_self_undefined_method_diagnostic(path, miss)
|
|
699
|
+
Diagnostic.new(
|
|
700
|
+
path: path,
|
|
701
|
+
line: miss.line || 1,
|
|
702
|
+
column: miss.column || 1,
|
|
703
|
+
message: "implicit-self call to `#{miss.method_name}` resolves to no method on " \
|
|
704
|
+
"`#{miss.class_name}` (a standalone class with a complete, project-known " \
|
|
705
|
+
"method surface). Likely a typo or a missing `def`.",
|
|
706
|
+
severity: :warning,
|
|
707
|
+
rule: RULE_SELF_UNDEFINED_METHOD,
|
|
708
|
+
source_family: :builtin,
|
|
709
|
+
receiver_type: miss.class_name,
|
|
710
|
+
method_name: miss.method_name
|
|
711
|
+
)
|
|
712
|
+
end
|
|
713
|
+
|
|
608
714
|
def lookup_method(receiver_type, class_name, method_name, scope)
|
|
609
715
|
if receiver_type.is_a?(Type::Singleton)
|
|
610
716
|
Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
|
|
@@ -832,11 +938,9 @@ module Rigor
|
|
|
832
938
|
return nil if inside_rigor_testing?(scope)
|
|
833
939
|
|
|
834
940
|
type = scope.type_of(arg)
|
|
835
|
-
|
|
836
|
-
|
|
941
|
+
Diagnostic.from_message_loc(
|
|
942
|
+
call_node,
|
|
837
943
|
path: path,
|
|
838
|
-
line: location.start_line,
|
|
839
|
-
column: location.start_column + 1,
|
|
840
944
|
message: "dump_type: #{type.describe(:short)}",
|
|
841
945
|
severity: :info,
|
|
842
946
|
rule: RULE_DUMP_TYPE
|
|
@@ -929,24 +1033,20 @@ module Rigor
|
|
|
929
1033
|
end
|
|
930
1034
|
|
|
931
1035
|
def build_assert_type_diagnostic(path, call_node, expected, actual)
|
|
932
|
-
|
|
933
|
-
|
|
1036
|
+
Diagnostic.from_message_loc(
|
|
1037
|
+
call_node,
|
|
934
1038
|
rule: RULE_ASSERT_TYPE,
|
|
935
1039
|
path: path,
|
|
936
|
-
line: location.start_line,
|
|
937
|
-
column: location.start_column + 1,
|
|
938
1040
|
message: "assert_type mismatch: expected #{expected.inspect}, got #{actual.inspect}",
|
|
939
1041
|
severity: :error
|
|
940
1042
|
)
|
|
941
1043
|
end
|
|
942
1044
|
|
|
943
1045
|
def build_nil_receiver_diagnostic(path, call_node)
|
|
944
|
-
|
|
945
|
-
|
|
1046
|
+
Diagnostic.from_message_loc(
|
|
1047
|
+
call_node,
|
|
946
1048
|
rule: RULE_NIL_RECEIVER,
|
|
947
1049
|
path: path,
|
|
948
|
-
line: location.start_line,
|
|
949
|
-
column: location.start_column + 1,
|
|
950
1050
|
message: "possible nil receiver: `#{call_node.name}' is undefined on NilClass",
|
|
951
1051
|
severity: :error
|
|
952
1052
|
)
|
|
@@ -1009,12 +1109,10 @@ module Rigor
|
|
|
1009
1109
|
end
|
|
1010
1110
|
|
|
1011
1111
|
def build_always_raises_diagnostic(path, call_node)
|
|
1012
|
-
|
|
1013
|
-
|
|
1112
|
+
Diagnostic.from_message_loc(
|
|
1113
|
+
call_node,
|
|
1014
1114
|
rule: RULE_ALWAYS_RAISES,
|
|
1015
1115
|
path: path,
|
|
1016
|
-
line: location.start_line,
|
|
1017
|
-
column: location.start_column + 1,
|
|
1018
1116
|
message: "always raises ZeroDivisionError: `#{call_node.name}' by zero on Integer receiver",
|
|
1019
1117
|
severity: :error
|
|
1020
1118
|
)
|
|
@@ -1133,12 +1231,10 @@ module Rigor
|
|
|
1133
1231
|
end
|
|
1134
1232
|
|
|
1135
1233
|
def build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
|
|
1136
|
-
|
|
1137
|
-
|
|
1234
|
+
Diagnostic.from_message_loc(
|
|
1235
|
+
call_node,
|
|
1138
1236
|
rule: RULE_VISIBILITY_MISMATCH,
|
|
1139
1237
|
path: path,
|
|
1140
|
-
line: location.start_line,
|
|
1141
|
-
column: location.start_column + 1,
|
|
1142
1238
|
message: "private method `#{call_node.name}' called on #{receiver_type.class_name} receiver",
|
|
1143
1239
|
severity: :error
|
|
1144
1240
|
)
|
|
@@ -1166,36 +1262,55 @@ module Rigor
|
|
|
1166
1262
|
end
|
|
1167
1263
|
|
|
1168
1264
|
def build_always_truthy_condition_diagnostic(path, predicate_node, polarity)
|
|
1169
|
-
|
|
1170
|
-
|
|
1265
|
+
Diagnostic.from_node(
|
|
1266
|
+
predicate_node,
|
|
1171
1267
|
rule: RULE_ALWAYS_TRUTHY_CONDITION,
|
|
1172
1268
|
path: path,
|
|
1173
|
-
line: location.start_line,
|
|
1174
|
-
column: location.start_column + 1,
|
|
1175
1269
|
message: "condition is always #{polarity} (the surrounding flow proves it folds to a constant)",
|
|
1176
1270
|
severity: :warning
|
|
1177
1271
|
)
|
|
1178
1272
|
end
|
|
1179
1273
|
|
|
1274
|
+
def build_unreachable_clause_diagnostic(path, result)
|
|
1275
|
+
Diagnostic.from_node(
|
|
1276
|
+
result.body,
|
|
1277
|
+
rule: RULE_UNREACHABLE_CLAUSE,
|
|
1278
|
+
path: path,
|
|
1279
|
+
message: unreachable_clause_message(result),
|
|
1280
|
+
severity: :warning
|
|
1281
|
+
)
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
def unreachable_clause_message(result)
|
|
1285
|
+
subject = result.subject_name
|
|
1286
|
+
kw = result.keyword
|
|
1287
|
+
case result.kind
|
|
1288
|
+
when :prior_exhaustion
|
|
1289
|
+
"unreachable `#{kw} #{result.condition_source}': `#{subject}' is already covered " \
|
|
1290
|
+
"by an earlier `#{kw}' clause"
|
|
1291
|
+
when :exhausted_else
|
|
1292
|
+
"unreachable `else': the `#{kw}' clauses already cover every value `#{subject}' can take here"
|
|
1293
|
+
else # :disjoint
|
|
1294
|
+
"unreachable `#{kw} #{result.condition_source}': `#{subject}' can never be " \
|
|
1295
|
+
"#{result.condition_source} here (the flow proves the subject disjoint)"
|
|
1296
|
+
end
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1180
1299
|
def build_dead_assignment_diagnostic(path, write_node, def_node)
|
|
1181
|
-
|
|
1182
|
-
|
|
1300
|
+
Diagnostic.from_name_loc(
|
|
1301
|
+
write_node,
|
|
1183
1302
|
rule: RULE_DEAD_ASSIGNMENT,
|
|
1184
1303
|
path: path,
|
|
1185
|
-
line: location.start_line,
|
|
1186
|
-
column: location.start_column + 1,
|
|
1187
1304
|
message: "local `#{write_node.name}' assigned in `#{def_node.name}' but never read",
|
|
1188
1305
|
severity: :warning
|
|
1189
1306
|
)
|
|
1190
1307
|
end
|
|
1191
1308
|
|
|
1192
1309
|
def build_ivar_write_mismatch_diagnostic(path, node, class_name, ivar_name, first_class, other_class)
|
|
1193
|
-
|
|
1194
|
-
|
|
1310
|
+
Diagnostic.from_name_loc(
|
|
1311
|
+
node,
|
|
1195
1312
|
rule: RULE_IVAR_WRITE_MISMATCH,
|
|
1196
1313
|
path: path,
|
|
1197
|
-
line: location.start_line,
|
|
1198
|
-
column: location.start_column + 1,
|
|
1199
1314
|
message: "instance variable `#{ivar_name}' on #{class_name} was previously assigned " \
|
|
1200
1315
|
"#{first_class}; this write assigns #{other_class}",
|
|
1201
1316
|
severity: :error
|
|
@@ -1214,12 +1329,10 @@ module Rigor
|
|
|
1214
1329
|
end
|
|
1215
1330
|
|
|
1216
1331
|
def build_unreachable_branch_diagnostic(path, dead_branch, polarity)
|
|
1217
|
-
|
|
1218
|
-
|
|
1332
|
+
Diagnostic.from_node(
|
|
1333
|
+
dead_branch,
|
|
1219
1334
|
rule: RULE_UNREACHABLE_BRANCH,
|
|
1220
1335
|
path: path,
|
|
1221
|
-
line: location.start_line,
|
|
1222
|
-
column: location.start_column + 1,
|
|
1223
1336
|
message: "unreachable branch: literal predicate is always #{polarity}",
|
|
1224
1337
|
severity: :warning
|
|
1225
1338
|
)
|
|
@@ -1348,39 +1461,34 @@ module Rigor
|
|
|
1348
1461
|
end
|
|
1349
1462
|
|
|
1350
1463
|
def build_argument_type_diagnostic(path, call_node, class_name, mismatch)
|
|
1351
|
-
location = mismatch[:node].location
|
|
1352
1464
|
method_label = "`#{call_node.name}' on #{class_name}"
|
|
1353
1465
|
parameter_label = mismatch[:name] ? "parameter `#{mismatch[:name]}' of #{method_label}" : method_label
|
|
1354
1466
|
message = "argument type mismatch at #{parameter_label}: " \
|
|
1355
1467
|
"expected #{mismatch[:expected].describe(:short)}, " \
|
|
1356
1468
|
"got #{mismatch[:actual].describe(:short)}"
|
|
1357
|
-
Diagnostic.
|
|
1469
|
+
Diagnostic.from_node(
|
|
1470
|
+
mismatch[:node],
|
|
1358
1471
|
rule: RULE_ARGUMENT_TYPE,
|
|
1359
1472
|
path: path,
|
|
1360
|
-
line: location.start_line,
|
|
1361
|
-
column: location.start_column + 1,
|
|
1362
1473
|
message: message,
|
|
1363
1474
|
severity: :error
|
|
1364
1475
|
)
|
|
1365
1476
|
end
|
|
1366
1477
|
|
|
1367
1478
|
def build_arity_diagnostic(path, call_node, class_name, min, max, actual)
|
|
1368
|
-
location = call_node.message_loc || call_node.location
|
|
1369
1479
|
range = min == max ? min.to_s : "#{min}..#{max}"
|
|
1370
1480
|
method_label = "`#{call_node.name}' on #{class_name}"
|
|
1371
1481
|
message = "wrong number of arguments to #{method_label} (given #{actual}, expected #{range})"
|
|
1372
|
-
Diagnostic.
|
|
1482
|
+
Diagnostic.from_message_loc(
|
|
1483
|
+
call_node,
|
|
1373
1484
|
rule: RULE_WRONG_ARITY,
|
|
1374
1485
|
path: path,
|
|
1375
|
-
line: location.start_line,
|
|
1376
|
-
column: location.start_column + 1,
|
|
1377
1486
|
message: message,
|
|
1378
1487
|
severity: :error
|
|
1379
1488
|
)
|
|
1380
1489
|
end
|
|
1381
1490
|
|
|
1382
1491
|
def build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site = nil, class_name = nil)
|
|
1383
|
-
location = call_node.message_loc || call_node.location
|
|
1384
1492
|
rendered_receiver = receiver_type.describe
|
|
1385
1493
|
message = "undefined method `#{call_node.name}' for #{rendered_receiver}"
|
|
1386
1494
|
# ADR-17 — when the project itself defines this method on the
|
|
@@ -1396,11 +1504,10 @@ module Rigor
|
|
|
1396
1504
|
"#{definition_site} — Rigor does not apply project monkey-patches " \
|
|
1397
1505
|
"cross-file; list that file in `.rigor.yml`'s `pre_eval:` (ADR-17)"
|
|
1398
1506
|
end
|
|
1399
|
-
Diagnostic.
|
|
1507
|
+
Diagnostic.from_message_loc(
|
|
1508
|
+
call_node,
|
|
1400
1509
|
rule: RULE_UNDEFINED_METHOD,
|
|
1401
1510
|
path: path,
|
|
1402
|
-
line: location.start_line,
|
|
1403
|
-
column: location.start_column + 1,
|
|
1404
1511
|
message: message,
|
|
1405
1512
|
severity: :error,
|
|
1406
1513
|
receiver_type: rendered_receiver,
|
|
@@ -1544,12 +1651,10 @@ module Rigor
|
|
|
1544
1651
|
end
|
|
1545
1652
|
|
|
1546
1653
|
def build_return_type_mismatch_diagnostic(path, def_node, declared, inferred, severity)
|
|
1547
|
-
|
|
1548
|
-
|
|
1654
|
+
Diagnostic.from_name_loc(
|
|
1655
|
+
def_node,
|
|
1549
1656
|
rule: RULE_RETURN_TYPE,
|
|
1550
1657
|
path: path,
|
|
1551
|
-
line: location.start_line,
|
|
1552
|
-
column: location.start_column + 1,
|
|
1553
1658
|
message: "return-type mismatch on `#{def_node.name}': " \
|
|
1554
1659
|
"declared #{declared.describe(:short)}, inferred #{inferred.describe(:short)}",
|
|
1555
1660
|
severity: severity
|
|
@@ -1679,6 +1784,14 @@ module Rigor
|
|
|
1679
1784
|
candidate = (segments[0, i] + [raw_ancestor]).join("::")
|
|
1680
1785
|
return candidate if known_user_class?(scope, candidate)
|
|
1681
1786
|
end
|
|
1787
|
+
# ADR-46 slice 3 — the override checker reads the class graph
|
|
1788
|
+
# directly (not through the recorder's `Scope` choke points), and
|
|
1789
|
+
# short-circuits when the ancestor resolves to no project class, so
|
|
1790
|
+
# an incremental re-check has no edge telling it to re-check this
|
|
1791
|
+
# subclass when that ancestor is later defined. Record a negative
|
|
1792
|
+
# class edge (keyed on the unqualified name) so the appeared-class
|
|
1793
|
+
# widening picks it up.
|
|
1794
|
+
DependencyRecorder.read_missing(:class, raw_ancestor.to_s.split("::").last) if DependencyRecorder.active?
|
|
1682
1795
|
nil
|
|
1683
1796
|
end
|
|
1684
1797
|
|
|
@@ -1689,12 +1802,10 @@ module Rigor
|
|
|
1689
1802
|
end
|
|
1690
1803
|
|
|
1691
1804
|
def build_override_visibility_diagnostic(path, def_node, parent_class, parent_visibility, override_visibility)
|
|
1692
|
-
|
|
1693
|
-
|
|
1805
|
+
Diagnostic.from_name_loc(
|
|
1806
|
+
def_node,
|
|
1694
1807
|
rule: RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
1695
1808
|
path: path,
|
|
1696
|
-
line: location.start_line,
|
|
1697
|
-
column: location.start_column + 1,
|
|
1698
1809
|
message: "visibility of `#{def_node.name}' reduced from #{parent_visibility} to " \
|
|
1699
1810
|
"#{override_visibility} (overrides #{parent_class}##{def_node.name}); " \
|
|
1700
1811
|
"breaks substitutability",
|
|
@@ -1784,12 +1895,10 @@ module Rigor
|
|
|
1784
1895
|
end
|
|
1785
1896
|
|
|
1786
1897
|
def build_override_return_widened_diagnostic(path, def_node, parent_class, parent_return, override_return)
|
|
1787
|
-
|
|
1788
|
-
|
|
1898
|
+
Diagnostic.from_name_loc(
|
|
1899
|
+
def_node,
|
|
1789
1900
|
rule: RULE_OVERRIDE_RETURN_WIDENED,
|
|
1790
1901
|
path: path,
|
|
1791
|
-
line: location.start_line,
|
|
1792
|
-
column: location.start_column + 1,
|
|
1793
1902
|
message: "return type of `#{def_node.name}' widened from #{parent_return.describe(:short)} " \
|
|
1794
1903
|
"to #{override_return.describe(:short)} (overrides #{parent_class}##{def_node.name}); " \
|
|
1795
1904
|
"breaks substitutability",
|
|
@@ -1890,12 +1999,10 @@ module Rigor
|
|
|
1890
1999
|
end
|
|
1891
2000
|
|
|
1892
2001
|
def build_override_param_narrowed_diagnostic(path, def_node, parent_class, index, parent_param, override_param)
|
|
1893
|
-
|
|
1894
|
-
|
|
2002
|
+
Diagnostic.from_name_loc(
|
|
2003
|
+
def_node,
|
|
1895
2004
|
rule: RULE_OVERRIDE_PARAM_NARROWED,
|
|
1896
2005
|
path: path,
|
|
1897
|
-
line: location.start_line,
|
|
1898
|
-
column: location.start_column + 1,
|
|
1899
2006
|
message: "parameter #{index + 1} of `#{def_node.name}' narrowed from " \
|
|
1900
2007
|
"#{parent_param.describe(:short)} to #{override_param.describe(:short)} " \
|
|
1901
2008
|
"(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
|
|
@@ -99,6 +99,24 @@ module Rigor
|
|
|
99
99
|
)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
# Builds a Diagnostic at a call node's `message_loc` (the
|
|
103
|
+
# method-name / matcher span), falling back to the receiver-
|
|
104
|
+
# spanning `node.location` when no message location is available.
|
|
105
|
+
# Absorbs the `node.message_loc || node.location` idiom the
|
|
106
|
+
# call-related rules otherwise repeat; all other fields forward to
|
|
107
|
+
# {.from_location}.
|
|
108
|
+
def self.from_message_loc(node, **)
|
|
109
|
+
from_location(node.message_loc || node.location, **)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Builds a Diagnostic at a definition / assignment node's
|
|
113
|
+
# `name_loc` (the declared name span), falling back to
|
|
114
|
+
# `node.location`. Absorbs the `node.name_loc || node.location`
|
|
115
|
+
# idiom the def / write rules otherwise repeat.
|
|
116
|
+
def self.from_name_loc(node, **)
|
|
117
|
+
from_location(node.name_loc || node.location, **)
|
|
118
|
+
end
|
|
119
|
+
|
|
102
120
|
def error?
|
|
103
121
|
severity == :error
|
|
104
122
|
end
|