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
|
@@ -476,31 +476,19 @@ module Rigor
|
|
|
476
476
|
# carriers like `Nominal[Integer]` (Integer is always truthy
|
|
477
477
|
# in Ruby — including 0) also collapse the dead else.
|
|
478
478
|
def live_branch_for_if(node, pred_type, post_pred)
|
|
479
|
-
case predicate_certainty(pred_type)
|
|
480
|
-
when :
|
|
481
|
-
when :
|
|
479
|
+
case Narrowing.predicate_certainty(pred_type)
|
|
480
|
+
when :truthy then eval_branch_or_nil(node.statements, post_pred)
|
|
481
|
+
when :falsey then eval_branch_or_nil(node.subsequent, post_pred)
|
|
482
482
|
end
|
|
483
483
|
end
|
|
484
484
|
|
|
485
485
|
def live_branch_for_unless(node, pred_type, post_pred)
|
|
486
|
-
case predicate_certainty(pred_type)
|
|
487
|
-
when :
|
|
488
|
-
when :
|
|
486
|
+
case Narrowing.predicate_certainty(pred_type)
|
|
487
|
+
when :truthy then eval_branch_or_nil(node.else_clause, post_pred)
|
|
488
|
+
when :falsey then eval_branch_or_nil(node.statements, post_pred)
|
|
489
489
|
end
|
|
490
490
|
end
|
|
491
491
|
|
|
492
|
-
def predicate_certainty(pred_type)
|
|
493
|
-
return nil if pred_type.nil? || pred_type.is_a?(Type::Bot)
|
|
494
|
-
|
|
495
|
-
truthy_bot = Narrowing.narrow_truthy(pred_type).is_a?(Type::Bot)
|
|
496
|
-
falsey_bot = Narrowing.narrow_falsey(pred_type).is_a?(Type::Bot)
|
|
497
|
-
|
|
498
|
-
return :always_falsey if truthy_bot && !falsey_bot
|
|
499
|
-
return :always_truthy if !truthy_bot && falsey_bot
|
|
500
|
-
|
|
501
|
-
nil
|
|
502
|
-
end
|
|
503
|
-
|
|
504
492
|
def eval_else(node)
|
|
505
493
|
return [Type::Combinator.constant_of(nil), scope] if node.statements.nil?
|
|
506
494
|
|
|
@@ -528,26 +516,76 @@ module Rigor
|
|
|
528
516
|
results = []
|
|
529
517
|
falsey_scope = entry_scope
|
|
530
518
|
conditions.each do |branch|
|
|
519
|
+
# ADR-47 WD2 — record the scope ENTERING this clause (the
|
|
520
|
+
# subject narrowed by every earlier clause's negation) on the
|
|
521
|
+
# clause's first condition node, so `flow.unreachable-clause`
|
|
522
|
+
# can tell a prior-exhausted subject (entry already `bot`)
|
|
523
|
+
# from a per-clause-disjoint one (entry concrete, this clause
|
|
524
|
+
# disjoint). `on_enter`-only (no recursion) so no condition
|
|
525
|
+
# sub-expression is newly typed; `propagate` preserves the
|
|
526
|
+
# entry because it already keys the node.
|
|
527
|
+
record_clause_entry_scope(branch, falsey_scope)
|
|
531
528
|
body_scope, falsey_scope = branch_body_and_falsey_scopes(subject, branch, falsey_scope)
|
|
532
529
|
results << sub_eval(branch, body_scope)
|
|
533
530
|
end
|
|
534
531
|
[results, falsey_scope]
|
|
535
532
|
end
|
|
536
533
|
|
|
534
|
+
# ADR-47 WD2/WD3 — record the scope ENTERING a `when`/`in` clause on
|
|
535
|
+
# the node `flow.unreachable-clause` reads to classify a dead clause
|
|
536
|
+
# (`when`: first condition; `in`: the pattern). `on_enter`-only so no
|
|
537
|
+
# sub-expression is newly typed; `propagate` preserves it.
|
|
538
|
+
def record_clause_entry_scope(branch, entry_scope)
|
|
539
|
+
node =
|
|
540
|
+
case branch
|
|
541
|
+
when Prism::WhenNode then branch.conditions.first
|
|
542
|
+
when Prism::InNode then branch.pattern
|
|
543
|
+
end
|
|
544
|
+
@on_enter&.call(node, entry_scope) if node
|
|
545
|
+
end
|
|
546
|
+
|
|
537
547
|
# Returns `[body_scope, updated_falsey_scope]` for a single branch.
|
|
538
|
-
# `
|
|
539
|
-
# narrow
|
|
540
|
-
#
|
|
541
|
-
#
|
|
548
|
+
# `WhenNode` branches narrow through `Narrowing.case_when_scopes`.
|
|
549
|
+
# `InNode` branches narrow soundly only for a bare class pattern
|
|
550
|
+
# (`in C` / `in C => x`, pure `is_a?`); every other pattern keeps
|
|
551
|
+
# the conservative "body = entry + bindings, falsey unchanged" shape.
|
|
542
552
|
def branch_body_and_falsey_scopes(subject, branch, falsey_scope)
|
|
543
553
|
if branch.is_a?(Prism::InNode)
|
|
544
|
-
|
|
554
|
+
in_branch_body_and_falsey_scopes(subject, branch, falsey_scope)
|
|
545
555
|
else
|
|
546
556
|
when_conditions = branch.respond_to?(:conditions) ? branch.conditions : []
|
|
547
557
|
Narrowing.case_when_scopes(subject, when_conditions, falsey_scope)
|
|
548
558
|
end
|
|
549
559
|
end
|
|
550
560
|
|
|
561
|
+
# ADR-47 WD3a — a bare class pattern matches on `C === subject`, i.e.
|
|
562
|
+
# exactly `subject.is_a?(C)` with no deconstruction, so it narrows
|
|
563
|
+
# like `when C`: the body sees the subject narrowed to `C` and the
|
|
564
|
+
# next clause's falsey scope has `C` removed. Other patterns can fail
|
|
565
|
+
# to match even when a class test would pass (deconstruction arity,
|
|
566
|
+
# hash keys, ...), so removing anything from the falsey scope would
|
|
567
|
+
# be unsound — they keep the conservative shape.
|
|
568
|
+
def in_branch_body_and_falsey_scopes(subject, branch, falsey_scope)
|
|
569
|
+
class_node = bare_class_pattern_node(branch.pattern)
|
|
570
|
+
return [apply_in_pattern_bindings(subject, branch.pattern, falsey_scope), falsey_scope] unless class_node
|
|
571
|
+
|
|
572
|
+
truthy_scope, narrowed_falsey = Narrowing.case_when_scopes(subject, [class_node], falsey_scope)
|
|
573
|
+
[apply_in_pattern_bindings(subject, branch.pattern, truthy_scope), narrowed_falsey]
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# The class-constant node of a `in C` / `in C => x` pattern (the only
|
|
577
|
+
# `in` shapes whose match is pure `is_a?`), or nil for any pattern
|
|
578
|
+
# that deconstructs, binds, or matches a value.
|
|
579
|
+
def bare_class_pattern_node(pattern)
|
|
580
|
+
case pattern
|
|
581
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
582
|
+
pattern
|
|
583
|
+
when Prism::CapturePatternNode
|
|
584
|
+
value = pattern.value
|
|
585
|
+
value if value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
551
589
|
def eval_case_else(else_clause, falsey_scope)
|
|
552
590
|
return sub_eval(else_clause, falsey_scope) if else_clause
|
|
553
591
|
|
|
@@ -1371,22 +1409,47 @@ module Rigor
|
|
|
1371
1409
|
end
|
|
1372
1410
|
end
|
|
1373
1411
|
|
|
1374
|
-
# ADR-37 slice 2 — gathers each plugin's post-return
|
|
1375
|
-
#
|
|
1376
|
-
# facts-only `FlowContribution
|
|
1377
|
-
# `flow_contribution_for` escape valve, swallowing per-plugin
|
|
1412
|
+
# ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's post-return
|
|
1413
|
+
# narrowing from the method-gated `type_specifier` DSL, wrapped as
|
|
1414
|
+
# a facts-only `FlowContribution`, swallowing per-plugin
|
|
1378
1415
|
# exceptions so a buggy plugin can't abort the assertion path.
|
|
1416
|
+
EMPTY_CONTRIBUTIONS = [].freeze
|
|
1417
|
+
private_constant :EMPTY_CONTRIBUTIONS
|
|
1418
|
+
|
|
1419
|
+
# Per-dispatch collection of plugin narrowing contributions. Mirrors
|
|
1420
|
+
# `MethodDispatcher#collect_plugin_contributions`: visit only the
|
|
1421
|
+
# registry-ordered subset of plugins that implement a per-call path
|
|
1422
|
+
# (`for_statement` = declares a `type_specifier`), gate each path
|
|
1423
|
+
# by membership AND by the ADR-52 WD1 method-name gates (every
|
|
1424
|
+
# `type_specifier` rule is `methods:`-gated, so the common
|
|
1425
|
+
# no-candidate case is a single Set probe; a pruned
|
|
1426
|
+
# consultation could only have returned `[]`), and accumulate
|
|
1427
|
+
# lazily (shared frozen empty array otherwise). Same contributions in
|
|
1428
|
+
# the same order as visiting every plugin; the caller is read-only.
|
|
1379
1429
|
def collect_plugin_contributions(registry, call_node, current_scope)
|
|
1380
|
-
registry.
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1430
|
+
index = registry.contribution_index
|
|
1431
|
+
relevant = index.for_statement
|
|
1432
|
+
return EMPTY_CONTRIBUTIONS if relevant.empty?
|
|
1433
|
+
|
|
1434
|
+
name = call_node.respond_to?(:name) ? call_node.name : nil
|
|
1435
|
+
return EMPTY_CONTRIBUTIONS unless index.statement_candidate?(name)
|
|
1436
|
+
|
|
1437
|
+
collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1440
|
+
# The post-gate walk, in registry order — the same order the
|
|
1441
|
+
# ungated walk used.
|
|
1442
|
+
def collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1443
|
+
result = nil
|
|
1444
|
+
relevant.each do |plugin|
|
|
1445
|
+
next unless index.type_specifier_candidate_for?(plugin, name)
|
|
1446
|
+
|
|
1384
1447
|
facts = plugin.type_specifier_facts(call_node: call_node, scope: current_scope)
|
|
1385
|
-
|
|
1386
|
-
contributions
|
|
1448
|
+
(result ||= []) << Rigor::FlowContribution.new(post_return_facts: facts) if facts && !facts.empty?
|
|
1387
1449
|
rescue StandardError
|
|
1388
|
-
|
|
1450
|
+
next
|
|
1389
1451
|
end
|
|
1452
|
+
result || EMPTY_CONTRIBUTIONS
|
|
1390
1453
|
end
|
|
1391
1454
|
|
|
1392
1455
|
def resolve_call_method(call_node, current_scope)
|
|
@@ -1766,20 +1829,19 @@ module Rigor
|
|
|
1766
1829
|
# ScopeIndexer-populated declaration overrides
|
|
1767
1830
|
# (`Prism::ConstantReadNode` for `module Foo` headers, etc.)
|
|
1768
1831
|
# remain reachable from inside nested bodies.
|
|
1769
|
-
def build_fresh_body_scope
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
.with_discovered_method_visibilities(scope.discovered_method_visibilities)
|
|
1832
|
+
def build_fresh_body_scope
|
|
1833
|
+
# Single allocation instead of a deep `with_*` chain — this runs
|
|
1834
|
+
# per class/method body on the main walk, so the chain's throwaway
|
|
1835
|
+
# intermediate Scopes were a top `Scope#rebuild` source (ADR-44).
|
|
1836
|
+
# Local-empty by design; the discovery index is inherited whole by
|
|
1837
|
+
# reference (ADR-53 Track A), so a table added to the index can no
|
|
1838
|
+
# longer be dropped here by a missed per-field copy.
|
|
1839
|
+
Scope.new(
|
|
1840
|
+
environment: scope.environment,
|
|
1841
|
+
locals: {}.freeze,
|
|
1842
|
+
source_path: scope.source_path,
|
|
1843
|
+
discovery: scope.discovery
|
|
1844
|
+
)
|
|
1783
1845
|
end
|
|
1784
1846
|
|
|
1785
1847
|
def singleton_def?(def_node)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "uri"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module LanguageServer
|
|
7
|
+
# Shared buffer lookup for the LSP providers.
|
|
8
|
+
#
|
|
9
|
+
# Every provider's `provide` opens with the same two steps: translate
|
|
10
|
+
# the document URI to an on-disk path, then fetch the open buffer
|
|
11
|
+
# entry from the buffer table — returning nil to the editor when
|
|
12
|
+
# either is missing. The parse step that follows differs per provider
|
|
13
|
+
# (strict vs error-tolerant, whole-buffer vs cursor-recovery), so
|
|
14
|
+
# only this resolution is shared here.
|
|
15
|
+
module BufferResolution
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Resolves `[path, entry]` for the document `uri`, or nil when the
|
|
19
|
+
# uri has no file path or no open buffer. A caller that destructures
|
|
20
|
+
# `path, entry = buffer_for(uri)` can guard on `entry.nil?` to cover
|
|
21
|
+
# both misses (a nil return leaves both locals nil).
|
|
22
|
+
def buffer_for(uri)
|
|
23
|
+
path = Uri.to_path(uri)
|
|
24
|
+
return nil if path.nil?
|
|
25
|
+
|
|
26
|
+
entry = @buffer_table[uri]
|
|
27
|
+
return nil if entry.nil?
|
|
28
|
+
|
|
29
|
+
[path, entry]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
|
+
require_relative "buffer_resolution"
|
|
6
7
|
require_relative "../environment"
|
|
7
8
|
require_relative "../reflection"
|
|
8
9
|
require_relative "../scope"
|
|
@@ -35,6 +36,8 @@ module Rigor
|
|
|
35
36
|
#
|
|
36
37
|
# Slice 6 will add 7 (Class), 9 (Module), 21 (Constant).
|
|
37
38
|
class CompletionProvider # rubocop:disable Metrics/ClassLength
|
|
39
|
+
include BufferResolution
|
|
40
|
+
|
|
38
41
|
KIND_METHOD = 2
|
|
39
42
|
KIND_FIELD = 5
|
|
40
43
|
KIND_CLASS = 7
|
|
@@ -54,10 +57,7 @@ module Rigor
|
|
|
54
57
|
# means "we tried and got nothing".
|
|
55
58
|
def provide(uri:, line:, character:, trigger_character: nil)
|
|
56
59
|
_ = trigger_character # Trigger info logged-not-routed in v1.
|
|
57
|
-
path =
|
|
58
|
-
return nil if path.nil?
|
|
59
|
-
|
|
60
|
-
entry = @buffer_table[uri]
|
|
60
|
+
path, entry = buffer_for(uri)
|
|
61
61
|
return nil if entry.nil?
|
|
62
62
|
|
|
63
63
|
# Slice B4 — parse recovery. The common mid-edit buffer
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
|
+
require_relative "buffer_resolution"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module LanguageServer
|
|
@@ -18,6 +19,8 @@ module Rigor
|
|
|
18
19
|
# - Method (6) — `def m` inside a class / module
|
|
19
20
|
# - Function (12) — `def m` at top-level (no enclosing class)
|
|
20
21
|
class DocumentSymbolProvider
|
|
22
|
+
include BufferResolution
|
|
23
|
+
|
|
21
24
|
KIND_MODULE = 2
|
|
22
25
|
KIND_CLASS = 5
|
|
23
26
|
KIND_METHOD = 6
|
|
@@ -33,10 +36,7 @@ module Rigor
|
|
|
33
36
|
# doesn't parse cleanly enough to surface symbols — LSP
|
|
34
37
|
# clients fall back to no-outline in that case.
|
|
35
38
|
def provide(uri)
|
|
36
|
-
path =
|
|
37
|
-
return nil if path.nil?
|
|
38
|
-
|
|
39
|
-
entry = @buffer_table[uri]
|
|
39
|
+
path, entry = buffer_for(uri)
|
|
40
40
|
return nil if entry.nil?
|
|
41
41
|
|
|
42
42
|
parse_result = Prism.parse(entry.bytes, filepath: path,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
|
+
require_relative "buffer_resolution"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module LanguageServer
|
|
@@ -18,6 +19,8 @@ module Rigor
|
|
|
18
19
|
# collapsed view shows the opener intact and hides the body
|
|
19
20
|
# only.
|
|
20
21
|
class FoldingRangeProvider
|
|
22
|
+
include BufferResolution
|
|
23
|
+
|
|
21
24
|
def initialize(buffer_table:, project_context:)
|
|
22
25
|
@buffer_table = buffer_table
|
|
23
26
|
@project_context = project_context
|
|
@@ -26,10 +29,7 @@ module Rigor
|
|
|
26
29
|
# @return [Array<Hash>, nil] LSP `FoldingRange[]` for the
|
|
27
30
|
# buffer, or nil when the URI isn't open / parseable.
|
|
28
31
|
def provide(uri)
|
|
29
|
-
path =
|
|
30
|
-
return nil if path.nil?
|
|
31
|
-
|
|
32
|
-
entry = @buffer_table[uri]
|
|
32
|
+
path, entry = buffer_for(uri)
|
|
33
33
|
return nil if entry.nil?
|
|
34
34
|
|
|
35
35
|
parse_result = Prism.parse(entry.bytes, filepath: path,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
|
+
require_relative "buffer_resolution"
|
|
6
7
|
require_relative "hover_renderer"
|
|
7
8
|
require_relative "../environment"
|
|
8
9
|
require_relative "../scope"
|
|
@@ -22,6 +23,8 @@ module Rigor
|
|
|
22
23
|
# for ASCII source (UTF-16 conversion is queued, see design
|
|
23
24
|
# doc § "Open questions").
|
|
24
25
|
class HoverProvider
|
|
26
|
+
include BufferResolution
|
|
27
|
+
|
|
25
28
|
def initialize(buffer_table:, project_context:, renderer: HoverRenderer.new)
|
|
26
29
|
@buffer_table = buffer_table
|
|
27
30
|
@project_context = project_context
|
|
@@ -33,10 +36,7 @@ module Rigor
|
|
|
33
36
|
# maps to `result: null` per the LSP spec — clients
|
|
34
37
|
# suppress the hover popup in that case.
|
|
35
38
|
def provide(uri:, line:, character:)
|
|
36
|
-
path =
|
|
37
|
-
return nil if path.nil?
|
|
38
|
-
|
|
39
|
-
entry = @buffer_table[uri]
|
|
39
|
+
path, entry = buffer_for(uri)
|
|
40
40
|
return nil if entry.nil?
|
|
41
41
|
|
|
42
42
|
parse_result = Prism.parse(entry.bytes, filepath: path, version: @project_context.configuration.target_ruby)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
|
+
require_relative "buffer_resolution"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module LanguageServer
|
|
@@ -13,6 +14,8 @@ module Rigor
|
|
|
13
14
|
# one keystroke moves up the chain, another moves further out,
|
|
14
15
|
# all the way to the root.
|
|
15
16
|
class SelectionRangeProvider
|
|
17
|
+
include BufferResolution
|
|
18
|
+
|
|
16
19
|
def initialize(buffer_table:, project_context:)
|
|
17
20
|
@buffer_table = buffer_table
|
|
18
21
|
@project_context = project_context
|
|
@@ -23,10 +26,7 @@ module Rigor
|
|
|
23
26
|
# @return [Array<Hash>, nil] one `SelectionRange` per
|
|
24
27
|
# position, or nil when the URI / buffer isn't resolvable.
|
|
25
28
|
def provide(uri, positions)
|
|
26
|
-
path =
|
|
27
|
-
return nil if path.nil?
|
|
28
|
-
|
|
29
|
-
entry = @buffer_table[uri]
|
|
29
|
+
path, entry = buffer_for(uri)
|
|
30
30
|
return nil if entry.nil?
|
|
31
31
|
|
|
32
32
|
parse_result = Prism.parse(entry.bytes, filepath: path,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
|
+
require_relative "buffer_resolution"
|
|
6
7
|
require_relative "../environment"
|
|
7
8
|
require_relative "../reflection"
|
|
8
9
|
require_relative "../scope"
|
|
@@ -35,6 +36,8 @@ module Rigor
|
|
|
35
36
|
# active-parameter override per overload land in follow-up
|
|
36
37
|
# slices (queued in the design doc § "Out of scope for v2").
|
|
37
38
|
class SignatureHelpProvider
|
|
39
|
+
include BufferResolution
|
|
40
|
+
|
|
38
41
|
ARG_SENTINEL = "__rigor_lsp_arg_sentinel__"
|
|
39
42
|
private_constant :ARG_SENTINEL
|
|
40
43
|
|
|
@@ -47,10 +50,7 @@ module Rigor
|
|
|
47
50
|
# when the cursor isn't inside a resolvable method call.
|
|
48
51
|
def provide(uri:, line:, character:, context: nil)
|
|
49
52
|
_ = context # Trigger info accepted but not routed in v1.
|
|
50
|
-
path =
|
|
51
|
-
return nil if path.nil?
|
|
52
|
-
|
|
53
|
-
entry = @buffer_table[uri]
|
|
53
|
+
path, entry = buffer_for(uri)
|
|
54
54
|
return nil if entry.nil?
|
|
55
55
|
|
|
56
56
|
bytes, locate_at = parse_attempt_bytes(entry.bytes, line, character)
|
|
@@ -3,32 +3,40 @@
|
|
|
3
3
|
module Rigor
|
|
4
4
|
module Plugin
|
|
5
5
|
# ADR-38 declaration: "on `receiver_constraint` (and its
|
|
6
|
-
# subclasses), every method named in `methods`
|
|
7
|
-
#
|
|
8
|
-
# read-before-write nil
|
|
6
|
+
# subclasses), every method named in `methods` (def-form) or
|
|
7
|
+
# `block_methods` (block-form) also establishes instance-variable
|
|
8
|
+
# state — treat it like `initialize` for the read-before-write nil
|
|
9
|
+
# soundness gate."
|
|
10
|
+
#
|
|
11
|
+
# **Def-form** (`methods:`) — applies when the ivar write lives in a
|
|
12
|
+
# named `def` body. Example: Minitest `def setup; @conn = …; end`.
|
|
13
|
+
#
|
|
14
|
+
# **Block-form** (`block_methods:`) — applies when the ivar write
|
|
15
|
+
# lives in a block passed to a method call. Example: RSpec
|
|
16
|
+
# `before { @user = create(:user) }` / `let(:x) { @y = … }`.
|
|
17
|
+
# `ScopeIndexer` descends the block body of any `CallNode` whose
|
|
18
|
+
# method name is in `block_methods`, collecting ivar writes exactly
|
|
19
|
+
# as it would for a def-form initializer.
|
|
20
|
+
#
|
|
21
|
+
# At least one of `methods:` or `block_methods:` must be non-empty.
|
|
9
22
|
#
|
|
10
23
|
# Authored on a plugin manifest:
|
|
11
24
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
25
|
+
# # def-form (Minitest):
|
|
26
|
+
# AdditionalInitializer.new(
|
|
27
|
+
# receiver_constraint: "Minitest::Test",
|
|
28
|
+
# methods: [:setup]
|
|
29
|
+
# )
|
|
30
|
+
#
|
|
31
|
+
# # block-form (RSpec):
|
|
32
|
+
# AdditionalInitializer.new(
|
|
33
|
+
# receiver_constraint: "RSpec::ExampleGroup",
|
|
34
|
+
# block_methods: [:before, :let, :subject]
|
|
21
35
|
# )
|
|
22
36
|
#
|
|
23
37
|
# The Ruby analogue of PHPStan's `AdditionalConstructorsExtension`.
|
|
24
38
|
# `Rigor::Inference::ScopeIndexer` consults the aggregated set at
|
|
25
|
-
# its
|
|
26
|
-
# `methods` on a class that equals or inherits from
|
|
27
|
-
# `receiver_constraint` (matched via `Environment#class_ordering`,
|
|
28
|
-
# the same mechanism ADR-16 Tier A uses), the method's ivar writes
|
|
29
|
-
# are folded into the class's `init_writes` set, so a sibling
|
|
30
|
-
# method reading those ivars no longer gets a `Constant[nil]`
|
|
31
|
-
# widening.
|
|
39
|
+
# its read-before-write gate.
|
|
32
40
|
#
|
|
33
41
|
# The contribution can only ever *suppress* a nil widening — it
|
|
34
42
|
# never makes the analyzer stricter — so a missed or over-broad
|
|
@@ -39,38 +47,47 @@ module Rigor
|
|
|
39
47
|
#
|
|
40
48
|
# - `receiver_constraint` — fully-qualified class name (String).
|
|
41
49
|
# The entry applies to that class and its subclasses.
|
|
42
|
-
# - `methods` — Array of Symbol method names
|
|
43
|
-
#
|
|
50
|
+
# - `methods` — Array of Symbol `def`-form method names (may be
|
|
51
|
+
# empty when only block_methods is used).
|
|
52
|
+
# - `block_methods` — Array of Symbol call-with-block method names
|
|
53
|
+
# (may be empty when only methods is used).
|
|
44
54
|
#
|
|
45
55
|
# ## Ractor-shareability
|
|
46
56
|
#
|
|
47
|
-
#
|
|
48
|
-
# `Ractor.shareable?` returns true after `#initialize
|
|
49
|
-
# value object survives `Plugin::Registry.materialize` into a
|
|
50
|
-
# worker Ractor.
|
|
57
|
+
# All fields are frozen at construction (ADR-15 Phase 1);
|
|
58
|
+
# `Ractor.shareable?` returns true after `#initialize`.
|
|
51
59
|
class AdditionalInitializer
|
|
52
|
-
attr_reader :receiver_constraint, :methods
|
|
60
|
+
attr_reader :receiver_constraint, :methods, :block_methods
|
|
53
61
|
|
|
54
|
-
def initialize(receiver_constraint:, methods:)
|
|
62
|
+
def initialize(receiver_constraint:, methods: [], block_methods: [])
|
|
55
63
|
validate_receiver_constraint!(receiver_constraint)
|
|
56
|
-
|
|
64
|
+
validate_method_list!(methods, :methods)
|
|
65
|
+
validate_method_list!(block_methods, :block_methods)
|
|
66
|
+
validate_at_least_one!(methods, block_methods)
|
|
57
67
|
|
|
58
68
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
59
69
|
@methods = methods.map(&:to_sym).freeze
|
|
70
|
+
@block_methods = block_methods.map(&:to_sym).freeze
|
|
60
71
|
freeze
|
|
61
72
|
end
|
|
62
73
|
|
|
63
|
-
# True when `method_name` (a Symbol) is declared
|
|
64
|
-
# by this entry.
|
|
65
|
-
# responsibility (it needs the environment's class graph).
|
|
74
|
+
# True when `method_name` (a Symbol) is declared a def-form
|
|
75
|
+
# initializer by this entry.
|
|
66
76
|
def covers_method?(method_name)
|
|
67
77
|
methods.include?(method_name)
|
|
68
78
|
end
|
|
69
79
|
|
|
80
|
+
# True when `method_name` (a Symbol) is declared a block-form
|
|
81
|
+
# initializer by this entry.
|
|
82
|
+
def covers_block_method?(method_name)
|
|
83
|
+
block_methods.include?(method_name)
|
|
84
|
+
end
|
|
85
|
+
|
|
70
86
|
def to_h
|
|
71
87
|
{
|
|
72
88
|
"receiver_constraint" => receiver_constraint,
|
|
73
|
-
"methods" => methods.map(&:to_s)
|
|
89
|
+
"methods" => methods.map(&:to_s),
|
|
90
|
+
"block_methods" => block_methods.map(&:to_s)
|
|
74
91
|
}
|
|
75
92
|
end
|
|
76
93
|
|
|
@@ -93,16 +110,22 @@ module Rigor
|
|
|
93
110
|
"got #{value.inspect}"
|
|
94
111
|
end
|
|
95
112
|
|
|
96
|
-
def
|
|
97
|
-
if value.is_a?(Array) &&
|
|
98
|
-
|
|
99
|
-
return
|
|
100
|
-
end
|
|
113
|
+
def validate_method_list!(value, field)
|
|
114
|
+
return if value.is_a?(Array) &&
|
|
115
|
+
value.all? { |m| m.is_a?(Symbol) || (m.is_a?(String) && !m.empty?) }
|
|
101
116
|
|
|
102
117
|
raise ArgumentError,
|
|
103
|
-
"Plugin::AdditionalInitializer
|
|
118
|
+
"Plugin::AdditionalInitializer##{field} must be an Array of " \
|
|
104
119
|
"Symbol/non-empty String, got #{value.inspect}"
|
|
105
120
|
end
|
|
121
|
+
|
|
122
|
+
def validate_at_least_one!(methods, block_methods)
|
|
123
|
+
return unless methods.empty? && block_methods.empty?
|
|
124
|
+
|
|
125
|
+
raise ArgumentError,
|
|
126
|
+
"Plugin::AdditionalInitializer requires at least one of methods: or block_methods: " \
|
|
127
|
+
"to be non-empty"
|
|
128
|
+
end
|
|
106
129
|
end
|
|
107
130
|
end
|
|
108
131
|
end
|