rigortype 0.3.0 → 0.3.2
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 +10 -10
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +11 -5
- data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
- data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
- data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/06-classes.md +1 -1
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -247
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/manual/02-cli-reference.md +103 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +36 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +3 -2
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +79 -19
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/run_cache_key.rb +27 -1
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
- data/lib/rigor/analysis/runner.rb +80 -8
- data/lib/rigor/bleeding_edge.rb +132 -7
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +162 -0
- data/lib/rigor/cache/incremental_snapshot.rb +87 -18
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +99 -24
- data/lib/rigor/cli/check_command.rb +61 -34
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/doctor_command.rb +6 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
- data/lib/rigor/cli/fused_protection_report.rb +12 -3
- data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
- data/lib/rigor/cli/mutation_protection_report.rb +26 -5
- data/lib/rigor/cli/protection_renderer.rb +13 -0
- data/lib/rigor/cli/protection_report.rb +11 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
- data/lib/rigor/cli/sig_gen_command.rb +5 -4
- data/lib/rigor/cli/skill_command.rb +21 -1
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/default_libraries.rb +5 -4
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +38 -14
- data/lib/rigor/inference/expression_typer.rb +15 -0
- data/lib/rigor/inference/fork_map.rb +9 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +58 -0
- data/lib/rigor/inference/protection_scanner.rb +18 -2
- data/lib/rigor/inference/receiver_alias.rb +57 -0
- data/lib/rigor/inference/scope_indexer.rb +78 -27
- data/lib/rigor/inference/statement_evaluator.rb +40 -2
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +68 -6
- data/lib/rigor/language_server/diagnostic_publisher.rb +207 -4
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/project_context.rb +45 -0
- data/lib/rigor/language_server/publish_batcher.rb +82 -0
- data/lib/rigor/language_server/server.rb +38 -9
- data/lib/rigor/language_server.rb +2 -0
- data/lib/rigor/plugin/base.rb +29 -2
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
- data/lib/rigor/protection/dependency_closure.rb +59 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
- data/lib/rigor/protection/discovery_seed.rb +116 -0
- data/lib/rigor/protection/kill_signature.rb +31 -0
- data/lib/rigor/protection/mutation_cache.rb +355 -0
- data/lib/rigor/protection/mutation_scanner.rb +55 -11
- data/lib/rigor/protection/mutator.rb +26 -4
- data/lib/rigor/runtime/jit.rb +63 -1
- data/lib/rigor/scope/discovery_index.rb +9 -0
- data/lib/rigor/scope.rb +52 -8
- data/lib/rigor/sig_gen/generator.rb +130 -31
- data/lib/rigor/sig_gen/layout_index.rb +6 -0
- data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
- data/lib/rigor/sig_gen/renderer.rb +20 -1
- data/lib/rigor/sig_gen/write_result.rb +6 -4
- data/lib/rigor/sig_gen/writer.rb +195 -47
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +21 -0
- data/sig/rigor/plugin/base.rbs +4 -3
- data/sig/rigor/scope.rbs +8 -0
- metadata +23 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Which variables can a receiver EXPRESSION evaluate to?
|
|
8
|
+
#
|
|
9
|
+
# A receiver-fact invalidation (see {MutationWidening}) has to name the binding it invalidates,
|
|
10
|
+
# and the overwhelmingly common receiver — a bare `arr` / `@arr` read — names exactly one. But a
|
|
11
|
+
# receiver may also *select* among variables without naming any of them:
|
|
12
|
+
#
|
|
13
|
+
# (kind == :required ? required : optional)[key] = info
|
|
14
|
+
#
|
|
15
|
+
# The mutation lands on whichever of `required` / `optional` the ternary picked, so BOTH are
|
|
16
|
+
# possible targets and both must forget their literal shape. Reading only the syntactic head
|
|
17
|
+
# left both hashes carrying the empty `HashShape` the literal `{}` wrote, and a downstream
|
|
18
|
+
# `.empty?` then constant-folded into a false `flow.always-truthy-condition`
|
|
19
|
+
# ([#277](https://github.com/rigortype/rigor/issues/277)).
|
|
20
|
+
#
|
|
21
|
+
# The recursion covers only the forms whose value IS one of the sub-expressions: `if` / `unless`
|
|
22
|
+
# (including the ternary spelling), the short-circuit operators, and the transparent wrappers.
|
|
23
|
+
# Anything else — an index read (`declared[kind] << key`), a call result, a literal — names an
|
|
24
|
+
# object no binding can be attributed to and yields `[]`, which is what every receiver form
|
|
25
|
+
# outside the single-read case already contributed. The walk is depth-capped so a pathological
|
|
26
|
+
# nest cannot make receiver classification unbounded.
|
|
27
|
+
module ReceiverAlias
|
|
28
|
+
# Deep enough for any hand-written selection; a nest beyond it degrades to "names no binding".
|
|
29
|
+
WALK_DEPTH_CAP = 6
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @param node [Prism::Node, nil] the receiver expression.
|
|
34
|
+
# @param depth [Integer] recursion depth, internal.
|
|
35
|
+
# @return [Array<Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode>] every variable
|
|
36
|
+
# read the expression can evaluate to; empty when it can evaluate to none.
|
|
37
|
+
def candidates(node, depth = 0)
|
|
38
|
+
return [] if node.nil? || depth > WALK_DEPTH_CAP
|
|
39
|
+
|
|
40
|
+
case node
|
|
41
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then [node]
|
|
42
|
+
when Prism::ParenthesesNode then candidates(node.body, depth + 1)
|
|
43
|
+
when Prism::StatementsNode then candidates(node.body.last, depth + 1)
|
|
44
|
+
when Prism::ElseNode then candidates(node.statements, depth + 1)
|
|
45
|
+
when Prism::IfNode then branches(node.statements, node.subsequent, depth)
|
|
46
|
+
when Prism::UnlessNode then branches(node.statements, node.else_clause, depth)
|
|
47
|
+
when Prism::OrNode, Prism::AndNode then branches(node.left, node.right, depth)
|
|
48
|
+
else []
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def branches(first, second, depth)
|
|
53
|
+
candidates(first, depth + 1) + candidates(second, depth + 1)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1275,16 +1275,40 @@ module Rigor
|
|
|
1275
1275
|
end
|
|
1276
1276
|
|
|
1277
1277
|
# Merges two `class_name => { method => kind }` tables, unioning the per-class method maps (so a seeded cross-file
|
|
1278
|
-
# table and the current file's table combine instead of clobbering).
|
|
1278
|
+
# table and the current file's table combine instead of clobbering). A name recorded on both sides — an instance
|
|
1279
|
+
# `def` here and a `class << self` twin there — merges to {Scope::DiscoveryIndex::METHOD_KIND_BOTH} rather than
|
|
1280
|
+
# letting the overlay's kind win (#239).
|
|
1279
1281
|
def deep_merge_class_methods(base, overlay)
|
|
1280
1282
|
return overlay if base.nil? || base.empty?
|
|
1281
1283
|
return base if overlay.empty?
|
|
1282
1284
|
|
|
1283
1285
|
base.merge(overlay) do |_class_name, base_methods, overlay_methods|
|
|
1284
|
-
base_methods
|
|
1286
|
+
merge_method_kinds(base_methods, overlay_methods)
|
|
1285
1287
|
end
|
|
1286
1288
|
end
|
|
1287
1289
|
|
|
1290
|
+
# `{ method => kind }` union that promotes a kind disagreement to `METHOD_KIND_BOTH` instead of clobbering.
|
|
1291
|
+
def merge_method_kinds(base_methods, overlay_methods)
|
|
1292
|
+
base_methods.merge(overlay_methods) do |_method_name, base_kind, overlay_kind|
|
|
1293
|
+
base_kind == overlay_kind ? base_kind : Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
1294
|
+
end
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
# The single write path into a `class_name => { method => kind }` existence table. Every recorder goes through
|
|
1298
|
+
# it so a class that defines one name on both sides keeps both facts: the table is keyed by name alone, so a
|
|
1299
|
+
# bare assignment silently replaced the other side's kind and `Scope#discovered_method?` then answered false for
|
|
1300
|
+
# a method the source plainly defines (#239).
|
|
1301
|
+
def record_method_kind(accumulator, class_name, method_name, kind)
|
|
1302
|
+
table = (accumulator[class_name] ||= {})
|
|
1303
|
+
recorded = table[method_name]
|
|
1304
|
+
table[method_name] =
|
|
1305
|
+
if recorded.nil? || recorded == kind
|
|
1306
|
+
kind
|
|
1307
|
+
else
|
|
1308
|
+
Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
1309
|
+
end
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1288
1312
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1289
1313
|
# Combined `walk_methods` + `walk_def_nodes` descent. The two walks had identical class / module / singleton-class
|
|
1290
1314
|
# / meta-block traversals and both stopped at `DefNode`; the only divergences are leaf actions (recorded into the
|
|
@@ -1393,8 +1417,7 @@ module Rigor
|
|
|
1393
1417
|
return if members.empty?
|
|
1394
1418
|
|
|
1395
1419
|
class_name = qualified_prefix.join("::")
|
|
1396
|
-
|
|
1397
|
-
members.each { |member| table[member] ||= :instance }
|
|
1420
|
+
members.each { |member| record_method_kind(accumulator, class_name, member, :instance) }
|
|
1398
1421
|
end
|
|
1399
1422
|
|
|
1400
1423
|
# The Symbol member names of a `Data.define(*Symbol)` / `Struct.new(*Symbol [, keyword_init:])` call. For
|
|
@@ -1412,8 +1435,7 @@ module Rigor
|
|
|
1412
1435
|
class_name = qualified_prefix.join("::")
|
|
1413
1436
|
singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
1414
1437
|
kind = singleton ? :singleton : :instance
|
|
1415
|
-
accumulator
|
|
1416
|
-
accumulator[class_name][def_node.name] = kind
|
|
1438
|
+
record_method_kind(accumulator, class_name, def_node.name, kind)
|
|
1417
1439
|
end
|
|
1418
1440
|
|
|
1419
1441
|
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) is semantically equivalent to `def
|
|
@@ -1918,7 +1940,7 @@ module Rigor
|
|
|
1918
1940
|
class_name = qualified_prefix.join("::")
|
|
1919
1941
|
new_name = alias_node.new_name.unescaped.to_sym
|
|
1920
1942
|
kind = in_singleton_class ? :singleton : :instance
|
|
1921
|
-
(accumulator
|
|
1943
|
+
record_method_kind(accumulator, class_name, new_name, kind)
|
|
1922
1944
|
end
|
|
1923
1945
|
|
|
1924
1946
|
# Post-pass over the `def_nodes` accumulator: for every `alias` declaration inside a class body, if the original
|
|
@@ -1981,8 +2003,7 @@ module Rigor
|
|
|
1981
2003
|
return if method_name.nil?
|
|
1982
2004
|
|
|
1983
2005
|
class_name = qualified_prefix.join("::")
|
|
1984
|
-
accumulator
|
|
1985
|
-
accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance
|
|
2006
|
+
record_method_kind(accumulator, class_name, method_name, in_singleton_class ? :singleton : :instance)
|
|
1986
2007
|
end
|
|
1987
2008
|
|
|
1988
2009
|
# The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
|
|
@@ -2005,9 +2026,8 @@ module Rigor
|
|
|
2005
2026
|
base = literal_method_name(arg)
|
|
2006
2027
|
next if base.nil?
|
|
2007
2028
|
|
|
2008
|
-
accumulator
|
|
2009
|
-
accumulator
|
|
2010
|
-
accumulator[class_name][:"#{base}="] = kind if writer
|
|
2029
|
+
record_method_kind(accumulator, class_name, base, kind) if reader
|
|
2030
|
+
record_method_kind(accumulator, class_name, :"#{base}=", kind) if writer
|
|
2011
2031
|
end
|
|
2012
2032
|
end
|
|
2013
2033
|
|
|
@@ -2352,7 +2372,7 @@ module Rigor
|
|
|
2352
2372
|
file_index[:def_nodes].each { |cn, methods| (acc[:def_nodes][cn] ||= {}).merge!(methods) }
|
|
2353
2373
|
file_index[:singleton_def_nodes].each { |cn, methods| (acc[:singleton_def_nodes][cn] ||= {}).merge!(methods) }
|
|
2354
2374
|
file_index[:method_visibilities].each { |cn, table| (acc[:method_visibilities][cn] ||= {}).merge!(table) }
|
|
2355
|
-
file_index[:methods].each { |cn, table| (acc[:methods][cn]
|
|
2375
|
+
file_index[:methods].each { |cn, table| acc[:methods][cn] = merge_method_kinds(acc[:methods][cn] || {}, table) }
|
|
2356
2376
|
fold_def_sources(acc, :def_sources, file_index[:def_sources])
|
|
2357
2377
|
fold_def_sources(acc, :singleton_def_sources, file_index[:singleton_def_sources])
|
|
2358
2378
|
end
|
|
@@ -2468,10 +2488,18 @@ module Rigor
|
|
|
2468
2488
|
|
|
2469
2489
|
# Removes, per class, the method names that have a project `def` node, leaving only
|
|
2470
2490
|
# accessor/alias/define_method-introduced methods in the cross-file suppression table.
|
|
2491
|
+
#
|
|
2492
|
+
# `def_nodes` is the INSTANCE-side table, so a name recorded on both sides keeps its singleton half: that half
|
|
2493
|
+
# comes from a `def self.x` / `class << self` definition this rule never covered, and dropping it whole would
|
|
2494
|
+
# reintroduce #239's false `call.undefined-method` across files.
|
|
2471
2495
|
def subtract_def_methods(methods, def_nodes)
|
|
2472
2496
|
methods.each_with_object({}) do |(class_name, table), out|
|
|
2473
2497
|
defs = def_nodes[class_name] || {}
|
|
2474
|
-
kept = table.
|
|
2498
|
+
kept = table.each_with_object({}) do |(method_name, kind), acc|
|
|
2499
|
+
next acc[method_name] = kind unless defs.key?(method_name)
|
|
2500
|
+
|
|
2501
|
+
acc[method_name] = :singleton if kind == Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
2502
|
+
end
|
|
2475
2503
|
out[class_name] = kept unless kept.empty?
|
|
2476
2504
|
end
|
|
2477
2505
|
end
|
|
@@ -2517,7 +2545,7 @@ module Rigor
|
|
|
2517
2545
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
2518
2546
|
end
|
|
2519
2547
|
file_methods.each do |class_name, table|
|
|
2520
|
-
(acc[:methods][class_name]
|
|
2548
|
+
acc[:methods][class_name] = merge_method_kinds(acc[:methods][class_name] || {}, table)
|
|
2521
2549
|
end
|
|
2522
2550
|
end
|
|
2523
2551
|
|
|
@@ -2570,24 +2598,47 @@ module Rigor
|
|
|
2570
2598
|
node.rigor_each_child { |child| collect_class_decls(child, qualified_prefix, accumulator) }
|
|
2571
2599
|
end
|
|
2572
2600
|
|
|
2573
|
-
# T1 (template-corpora survey) — record a `Const = Class.new(Super)
|
|
2574
|
-
#
|
|
2575
|
-
#
|
|
2601
|
+
# T1 (template-corpora survey) — record a class-creating constant write (`Const = Class.new(Super)`, the bare
|
|
2602
|
+
# `Class.new` / `Module.new`, and the `Data.define(*sym)` / `Struct.new(*sym)` forms, each with or without a
|
|
2603
|
+
# block) in the cross-file discovery table so a reference to `Const` from ANOTHER file under the same namespace
|
|
2604
|
+
# resolves to the project class instead of falling through to a same-named class elsewhere
|
|
2576
2605
|
# (`Liquid::SyntaxError = Class.new(Error)` referenced in a sibling file's `rescue SyntaxError => e`, which
|
|
2577
2606
|
# otherwise resolved to core `::SyntaxError`). Mirrors the single-file `in_source_constants` answer, which types
|
|
2578
|
-
# `Class.new(Super)` as `Singleton[Super]` (the constructed class answers method lookups through Super's chain)
|
|
2579
|
-
#
|
|
2580
|
-
#
|
|
2581
|
-
#
|
|
2582
|
-
#
|
|
2607
|
+
# `Class.new(Super)` as `Singleton[Super]` (the constructed class answers method lookups through Super's chain)
|
|
2608
|
+
# and every other recognised form as `Singleton[Const]`.
|
|
2609
|
+
#
|
|
2610
|
+
# #271 — the Data/Struct forms are here because their absence was an ACTIVE false positive, not merely a missed
|
|
2611
|
+
# resolution: a nested `Const = Data.define(...)` invisible cross-file lets Ruby's lexical walk continue to the
|
|
2612
|
+
# PARENT namespace's same-named sibling, so `Analysis::PluginFactFingerprint.from_registry(...).opaque?` typed
|
|
2613
|
+
# its receiver as the unrelated `Rigor::Analysis::Result` and reported `call.undefined-method` on correct code.
|
|
2614
|
+
# Nested `Result` / `Entry` / `Config` Data constants shadowing a sibling are ordinary Ruby, and only the
|
|
2615
|
+
# DEFINING file's `in_source_constants` (never part of the project seed) knew about them.
|
|
2583
2616
|
def record_class_new_constant_decl(node, qualified_prefix, accumulator)
|
|
2584
2617
|
rvalue = node.value
|
|
2585
|
-
return unless
|
|
2586
|
-
return if rvalue.block # block form: handled by meta_new_block_body walks
|
|
2618
|
+
return unless meta_new_constant_rvalue?(rvalue)
|
|
2587
2619
|
|
|
2588
2620
|
full = (qualified_prefix + [node.name.to_s]).join("::")
|
|
2589
|
-
|
|
2590
|
-
|
|
2621
|
+
accumulator[full] = Type::Combinator.singleton_of(
|
|
2622
|
+
meta_new_constant_decl_name(rvalue, full, qualified_prefix, accumulator)
|
|
2623
|
+
)
|
|
2624
|
+
end
|
|
2625
|
+
|
|
2626
|
+
# The four recognised class-creating rvalue shapes at constant-write position — the same set
|
|
2627
|
+
# {#meta_new_block_body} recognises, so the cross-file table and the per-file block-as-method walk agree on what
|
|
2628
|
+
# counts as a declaration.
|
|
2629
|
+
def meta_new_constant_rvalue?(rvalue)
|
|
2630
|
+
class_new_call?(rvalue) || module_new_call?(rvalue) ||
|
|
2631
|
+
data_define_call?(rvalue) || struct_new_call?(rvalue)
|
|
2632
|
+
end
|
|
2633
|
+
|
|
2634
|
+
# The name the constant's `Singleton[...]` carries. Only a block-less `Class.new(Super)` borrows its superclass's
|
|
2635
|
+
# name (the constructed class answers lookups through `Super`'s chain and declares nothing of its own); every
|
|
2636
|
+
# other form — Data/Struct members, any block body — owns methods under its OWN qualified name, which is exactly
|
|
2637
|
+
# what the per-file `meta_new_constant_type` answers.
|
|
2638
|
+
def meta_new_constant_decl_name(rvalue, full, qualified_prefix, accumulator)
|
|
2639
|
+
return full if rvalue.block || data_define_call?(rvalue) || struct_new_call?(rvalue)
|
|
2640
|
+
|
|
2641
|
+
class_new_superclass_name(rvalue, qualified_prefix, accumulator) || full
|
|
2591
2642
|
end
|
|
2592
2643
|
|
|
2593
2644
|
# Lexically-qualified name of a `Class.new(Super)` superclass argument, or nil when there is no positional
|
|
@@ -247,9 +247,31 @@ module Rigor
|
|
|
247
247
|
|
|
248
248
|
bound = bound.without_inferred_param_mark(node.name)
|
|
249
249
|
bound = bound.with_local_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
|
|
250
|
+
bound = bound.with_optimistic_local(node.name, optimistic_rhs_origin(node.value, post_rhs))
|
|
250
251
|
[rhs_type, bound]
|
|
251
252
|
end
|
|
252
253
|
|
|
254
|
+
# Issue #286 — the optimistic-nil-free counterpart of {#rhs_origin}, differing in two ways. It does not
|
|
255
|
+
# gate on `Dynamic`: the values this channel marks are ordinary `Union` / `Constant` / `Nominal`
|
|
256
|
+
# carriers, which is the whole point. And it resolves a bare local read through its binding, so
|
|
257
|
+
# `w = v` keeps the mark — the same propagation `OriginLookup` performs for the Dynamic channel.
|
|
258
|
+
def optimistic_rhs_origin(value_node, scope_after_rhs)
|
|
259
|
+
optimistic_origin_for(value_node, scope_after_rhs)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The effective optimistic-nil-free cause of an expression: the mark on its own node, else — for a bare
|
|
263
|
+
# local read or a local write used in value position (`if (x = MAP[k])`, where the write has already
|
|
264
|
+
# bound the mark) — the one propagated onto the binding.
|
|
265
|
+
def optimistic_origin_for(node, scope)
|
|
266
|
+
recorded = scope.optimistic_origins[node]
|
|
267
|
+
return recorded if recorded
|
|
268
|
+
|
|
269
|
+
case node
|
|
270
|
+
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
|
|
271
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
253
275
|
# ADR-82 WD1 — the {Inference::DynamicOrigin} cause to propagate onto a local / ivar being bound to `rhs`.
|
|
254
276
|
# Returns the cause recorded on the assignment's rhs node when the value is `Dynamic` (so a later
|
|
255
277
|
# `x` / `@x` receiver-read resolves to why it is dynamic), else `nil` — `with_local_origin` /
|
|
@@ -277,6 +299,7 @@ module Rigor
|
|
|
277
299
|
rhs_type, post_rhs = sub_eval(node.value, scope)
|
|
278
300
|
bound = post_rhs.with_ivar(node.name, rhs_type)
|
|
279
301
|
bound = bound.with_ivar_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
|
|
302
|
+
bound = bound.with_optimistic_ivar(node.name, optimistic_rhs_origin(node.value, post_rhs))
|
|
280
303
|
[rhs_type, bound]
|
|
281
304
|
end
|
|
282
305
|
|
|
@@ -543,19 +566,34 @@ module Rigor
|
|
|
543
566
|
# non-falsey carriers like `Nominal[Integer]` (Integer is always truthy in Ruby — including 0) also collapse the
|
|
544
567
|
# dead else.
|
|
545
568
|
def live_branch_for_if(node, pred_type, post_pred)
|
|
546
|
-
|
|
569
|
+
verdict = optimistic_carrier?(node.predicate, post_pred) ? nil : Narrowing.predicate_certainty(pred_type)
|
|
570
|
+
case verdict
|
|
547
571
|
when :truthy then eval_branch_or_nil(node.statements, post_pred)
|
|
548
572
|
when :falsey then eval_branch_or_nil(node.subsequent, post_pred)
|
|
549
573
|
end
|
|
550
574
|
end
|
|
551
575
|
|
|
552
576
|
def live_branch_for_unless(node, pred_type, post_pred)
|
|
553
|
-
|
|
577
|
+
verdict = optimistic_carrier?(node.predicate, post_pred) ? nil : Narrowing.predicate_certainty(pred_type)
|
|
578
|
+
case verdict
|
|
554
579
|
when :truthy then eval_branch_or_nil(node.else_clause, post_pred)
|
|
555
580
|
when :falsey then eval_branch_or_nil(node.statements, post_pred)
|
|
556
581
|
end
|
|
557
582
|
end
|
|
558
583
|
|
|
584
|
+
# ADR-101 — the branch elision MUST NOT conclude truthiness from a carrier whose nil-freeness rests on
|
|
585
|
+
# the `%a{implicitly-returns-nil}` that `RbsDispatch` reads past. Such a value is optimistic, not proof
|
|
586
|
+
# (see {Inference::OptimisticOrigin} and docs/internal-spec/inference-engine.md), so eliding an arm on
|
|
587
|
+
# it deletes a branch the program really takes when the lookup misses.
|
|
588
|
+
#
|
|
589
|
+
# The decline lives here and NOT in `Narrowing.falsey_nominal?` / `.narrow_falsey`: `&&=` / `||=` and
|
|
590
|
+
# the and/or surviving-left edge read those too, and widening the falsey fragment there would re-admit
|
|
591
|
+
# `nil` into a bound local and buy `possible nil receiver` false positives — a soundness fix paid for
|
|
592
|
+
# in FPs, which is the wrong trade.
|
|
593
|
+
def optimistic_carrier?(predicate, scope)
|
|
594
|
+
!optimistic_origin_for(predicate, scope).nil?
|
|
595
|
+
end
|
|
596
|
+
|
|
559
597
|
def eval_else(node)
|
|
560
598
|
return [Type::Combinator.constant_of(nil), scope] if node.statements.nil?
|
|
561
599
|
|
|
@@ -13,12 +13,15 @@ module Rigor
|
|
|
13
13
|
module BufferResolution
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
-
# Resolves `[path, entry]` for the document `uri`, or nil when the uri has no file path
|
|
17
|
-
# buffer
|
|
18
|
-
#
|
|
16
|
+
# Resolves `[path, entry]` for the document `uri`, or nil when the uri has no file path, no open buffer,
|
|
17
|
+
# or a buffer the server could not keep in sync with the editor (see `BufferTable#apply_changes`) — a
|
|
18
|
+
# position answered from text that has drifted from the editor's is worse than no answer. A caller that
|
|
19
|
+
# destructures `path, entry = buffer_for(uri)` can guard on `entry.nil?` to cover every miss (a nil
|
|
20
|
+
# return leaves both locals nil).
|
|
19
21
|
def buffer_for(uri)
|
|
20
22
|
path = Uri.to_path(uri)
|
|
21
23
|
return nil if path.nil?
|
|
24
|
+
return nil if @buffer_table.desynchronized?(uri)
|
|
22
25
|
|
|
23
26
|
entry = @buffer_table[uri]
|
|
24
27
|
return nil if entry.nil?
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "incremental_sync"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module LanguageServer
|
|
5
7
|
# Per-session virtual file table. The LSP server maintains the canonical view of every open buffer here;
|
|
6
8
|
# analysis (slice 4+) reads from this table instead of disk so in-flight edits are reflected immediately.
|
|
7
9
|
#
|
|
8
|
-
# Keyed by `DocumentUri` (LSP `file://...` URIs).
|
|
9
|
-
# `TextDocumentSyncKind::
|
|
10
|
-
#
|
|
10
|
+
# Keyed by `DocumentUri` (LSP `file://...` URIs). Sync is INCREMENTAL (LSP
|
|
11
|
+
# `TextDocumentSyncKind::Incremental = 2`): a `didChange` carries range edits which {#apply_changes} splices
|
|
12
|
+
# into the held text through {IncrementalSync}. The full-text form ({#change}, and a `contentChanges` entry
|
|
13
|
+
# with no `range`) stays supported — it is still legal under incremental sync and is what `didOpen` and a
|
|
14
|
+
# client-side resync send.
|
|
11
15
|
class BufferTable
|
|
12
16
|
# @!attribute uri [String] the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
|
|
13
17
|
# @!attribute bytes [String] the current full text of the buffer.
|
|
@@ -16,26 +20,84 @@ module Rigor
|
|
|
16
20
|
|
|
17
21
|
def initialize
|
|
18
22
|
@entries = {}
|
|
23
|
+
@desynchronized = {}
|
|
24
|
+
@dirty = {}
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
# Records a `textDocument/didOpen` event. Replaces any existing entry (LSP clients may re-open a
|
|
22
|
-
# previously closed URI; the new version is authoritative)
|
|
28
|
+
# previously closed URI; the new version is authoritative) and clears any desynchronised mark — the
|
|
29
|
+
# payload carries the client's full text, so the two views agree again.
|
|
23
30
|
def open(uri:, bytes:, version:)
|
|
31
|
+
@desynchronized.delete(uri)
|
|
32
|
+
@dirty.delete(uri)
|
|
24
33
|
@entries[uri] = Entry.new(uri: uri, bytes: bytes, version: version)
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
# Records a `textDocument/didChange
|
|
28
|
-
#
|
|
36
|
+
# Records a full-text `textDocument/didChange`. The full new buffer text replaces the entry. If the
|
|
37
|
+
# client sends a `didChange` for a URI that was never opened (spec violation), the entry is still
|
|
29
38
|
# created — defensive.
|
|
30
39
|
def change(uri:, bytes:, version:)
|
|
40
|
+
@desynchronized.delete(uri)
|
|
41
|
+
@dirty[uri] = true
|
|
31
42
|
@entries[uri] = Entry.new(uri: uri, bytes: bytes, version: version)
|
|
32
43
|
end
|
|
33
44
|
|
|
45
|
+
# Applies a `textDocument/didChange` payload under INCREMENTAL sync. Each entry of `changes` is applied
|
|
46
|
+
# in order against the result of the previous one, per the LSP contract.
|
|
47
|
+
#
|
|
48
|
+
# On success the entry is replaced with the spliced text at the new version. On failure — a malformed
|
|
49
|
+
# change, or a range edit for a URI with no held buffer — the held text is left EXACTLY as it was and the
|
|
50
|
+
# URI is marked desynchronised: the server's view and the editor's view have diverged, and every position
|
|
51
|
+
# computed from the stale text would be wrong. Consumers check {#desynchronized?} and decline to answer
|
|
52
|
+
# rather than answering wrongly; the mark clears on the next `didOpen` or full-text change.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean] true when the changes were applied.
|
|
55
|
+
def apply_changes(uri:, changes:, version:)
|
|
56
|
+
text = IncrementalSync.apply_all(@entries[uri]&.bytes, changes)
|
|
57
|
+
@desynchronized.delete(uri)
|
|
58
|
+
@dirty[uri] = true
|
|
59
|
+
@entries[uri] = Entry.new(uri: uri, bytes: text, version: version)
|
|
60
|
+
true
|
|
61
|
+
rescue IncrementalSync::UnappliableChange => e
|
|
62
|
+
@desynchronized[uri] = e.message
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Boolean] true when the last `didChange` for `uri` could not be applied, so the held text no
|
|
67
|
+
# longer matches the editor's.
|
|
68
|
+
def desynchronized?(uri)
|
|
69
|
+
@desynchronized.key?(uri)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [String, nil] why `uri` is desynchronised, for the log line; nil when it is in sync.
|
|
73
|
+
def desynchronization_reason(uri)
|
|
74
|
+
@desynchronized[uri]
|
|
75
|
+
end
|
|
76
|
+
|
|
34
77
|
# Records a `textDocument/didClose` event. The entry is removed. Subsequent reads via `#[]` return nil.
|
|
35
78
|
def close(uri:)
|
|
79
|
+
@desynchronized.delete(uri)
|
|
80
|
+
@dirty.delete(uri)
|
|
36
81
|
@entries.delete(uri)
|
|
37
82
|
end
|
|
38
83
|
|
|
84
|
+
# Records a `textDocument/didSave`. The client has written the buffer, so the held text and the file on
|
|
85
|
+
# disk agree again and the URI stops being dirty.
|
|
86
|
+
#
|
|
87
|
+
# Dirtiness is the PROTOCOL's notion — "the client told us it changed and has not told us it saved" —
|
|
88
|
+
# not a byte comparison against disk. A comparison would look stricter and be weaker: it races with the
|
|
89
|
+
# editor's own write, and the server's truth is what the client notified.
|
|
90
|
+
def save(uri:)
|
|
91
|
+
@dirty.delete(uri)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Boolean] true when `uri` has unsaved changes. A buffer that is dirty may only be published
|
|
95
|
+
# from an analysis that bound ITS bytes — see the publish-set invariant in
|
|
96
|
+
# `docs/design/20260517-language-server.md`.
|
|
97
|
+
def dirty?(uri)
|
|
98
|
+
@dirty.key?(uri)
|
|
99
|
+
end
|
|
100
|
+
|
|
39
101
|
def [](uri)
|
|
40
102
|
@entries[uri]
|
|
41
103
|
end
|