rigortype 0.3.1 → 0.3.3

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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -10
  3. data/data/core_overlay/pathname.rbs +5 -0
  4. data/data/core_overlay/resolv.rbs +31 -0
  5. data/data/core_overlay/string_scanner.rbs +5 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
  8. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
  9. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
  10. data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
  11. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
  12. data/docs/manual/02-cli-reference.md +71 -11
  13. data/docs/manual/03-configuration.md +16 -1
  14. data/docs/manual/04-diagnostics.md +8 -0
  15. data/docs/manual/12-caching.md +17 -5
  16. data/docs/manual/15-type-protection-coverage.md +122 -1
  17. data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
  19. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
  20. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
  21. data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
  22. data/lib/rigor/analysis/check_rules.rb +107 -28
  23. data/lib/rigor/analysis/incremental_session.rb +156 -11
  24. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
  25. data/lib/rigor/analysis/rule_catalog.rb +5 -1
  26. data/lib/rigor/analysis/run_cache_key.rb +17 -1
  27. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
  28. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
  29. data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
  30. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
  31. data/lib/rigor/analysis/runner.rb +78 -7
  32. data/lib/rigor/analysis/severity_stamp.rb +2 -1
  33. data/lib/rigor/bleeding_edge.rb +132 -7
  34. data/lib/rigor/cache/descriptor.rb +6 -1
  35. data/lib/rigor/cache/engine_source.rb +162 -0
  36. data/lib/rigor/cache/incremental_snapshot.rb +87 -18
  37. data/lib/rigor/cli/check_command.rb +49 -28
  38. data/lib/rigor/cli/coverage_command.rb +15 -2
  39. data/lib/rigor/cli/coverage_mutation.rb +242 -11
  40. data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
  41. data/lib/rigor/cli/fused_protection_report.rb +12 -3
  42. data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
  43. data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
  44. data/lib/rigor/cli/mutation_protection_report.rb +26 -5
  45. data/lib/rigor/cli/protection_renderer.rb +13 -0
  46. data/lib/rigor/cli/protection_report.rb +11 -3
  47. data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
  48. data/lib/rigor/cli/sig_gen_command.rb +5 -4
  49. data/lib/rigor/configuration.rb +45 -6
  50. data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
  51. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  52. data/lib/rigor/environment/rbs_loader.rb +434 -75
  53. data/lib/rigor/environment.rb +28 -13
  54. data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
  55. data/lib/rigor/inference/expression_typer.rb +62 -4
  56. data/lib/rigor/inference/fork_map.rb +47 -4
  57. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
  58. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  59. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
  60. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +158 -12
  61. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
  62. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +234 -11
  63. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
  64. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
  65. data/lib/rigor/inference/method_dispatcher.rb +41 -10
  66. data/lib/rigor/inference/mutation_widening.rb +30 -25
  67. data/lib/rigor/inference/narrowing.rb +3 -1
  68. data/lib/rigor/inference/optimistic_origin.rb +123 -0
  69. data/lib/rigor/inference/protection_scanner.rb +18 -2
  70. data/lib/rigor/inference/receiver_alias.rb +57 -0
  71. data/lib/rigor/inference/scope_indexer.rb +197 -55
  72. data/lib/rigor/inference/singleton_object_constant.rb +63 -0
  73. data/lib/rigor/inference/statement_evaluator.rb +55 -4
  74. data/lib/rigor/language_server/buffer_table.rb +22 -0
  75. data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
  76. data/lib/rigor/language_server/project_context.rb +45 -0
  77. data/lib/rigor/language_server/publish_batcher.rb +82 -0
  78. data/lib/rigor/language_server/server.rb +20 -1
  79. data/lib/rigor/language_server.rb +1 -0
  80. data/lib/rigor/plugin/node_rule_walk.rb +6 -0
  81. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  82. data/lib/rigor/protection/closure_kill_oracle.rb +190 -0
  83. data/lib/rigor/protection/dependency_closure.rb +59 -0
  84. data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
  85. data/lib/rigor/protection/discovery_seed.rb +116 -0
  86. data/lib/rigor/protection/kill_signature.rb +31 -0
  87. data/lib/rigor/protection/mutation_cache.rb +355 -0
  88. data/lib/rigor/protection/mutation_scanner.rb +55 -11
  89. data/lib/rigor/protection/mutator.rb +26 -4
  90. data/lib/rigor/runtime/jit.rb +63 -1
  91. data/lib/rigor/scope/discovery_index.rb +9 -0
  92. data/lib/rigor/scope.rb +114 -10
  93. data/lib/rigor/sig_gen/generator.rb +130 -31
  94. data/lib/rigor/sig_gen/layout_index.rb +6 -0
  95. data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
  96. data/lib/rigor/sig_gen/renderer.rb +20 -1
  97. data/lib/rigor/sig_gen/write_result.rb +6 -4
  98. data/lib/rigor/sig_gen/writer.rb +12 -0
  99. data/lib/rigor/sig_gen.rb +1 -0
  100. data/lib/rigor/source/node_walker.rb +12 -0
  101. data/lib/rigor/type/anonymous_class_name.rb +40 -0
  102. data/lib/rigor/type/nominal.rb +5 -2
  103. data/lib/rigor/type/singleton.rb +4 -1
  104. data/lib/rigor/version.rb +1 -1
  105. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
  106. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
  107. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
  108. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
  109. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
  110. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
  111. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
  112. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
  113. data/sig/rigor/inference/void_origin.rbs +6 -3
  114. data/sig/rigor/scope.rbs +12 -0
  115. data/sig/rigor.rbs +4 -0
  116. metadata +21 -1
@@ -7,6 +7,7 @@ require_relative "../type"
7
7
  require_relative "../source/constant_path"
8
8
  require_relative "../source/node_children"
9
9
  require_relative "../cache/file_digest"
10
+ require_relative "anonymous_meta_class"
10
11
  require_relative "def_handle"
11
12
  require_relative "mutation_widening"
12
13
  require_relative "narrowing"
@@ -133,7 +134,7 @@ module Rigor
133
134
  # file_def_nodes]` so the caller can thread the def-node table into {#merge_project_method_indexes} without
134
135
  # walking the file a second time.
135
136
  def seed_discovered_methods(seeded_scope, default_scope, root)
136
- file_methods, file_def_nodes = build_methods_and_def_nodes(root)
137
+ file_methods, file_def_nodes = build_methods_and_def_nodes(root, default_scope.source_path)
137
138
  discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
138
139
  scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
139
140
  [scope, file_def_nodes]
@@ -161,7 +162,7 @@ module Rigor
161
162
  build_discovered_singleton_def_nodes(root)
162
163
  ) { |_class, cross_file, per_file| cross_file.merge(per_file) }
163
164
  superclasses = default_scope.discovered_superclasses.merge(
164
- build_discovered_superclasses(root)
165
+ build_discovered_superclasses(root, default_scope.source_path)
165
166
  )
166
167
  includes = default_scope.discovered_includes.merge(
167
168
  build_discovered_includes(root)
@@ -1266,32 +1267,57 @@ module Rigor
1266
1267
  # `walk_methods` and `walk_def_nodes` had byte-identical class / module / singleton / meta-block descents (both
1267
1268
  # stop at `DefNode`), so a single combined walk records both accumulators at once instead of traversing every file
1268
1269
  # twice.
1269
- def build_methods_and_def_nodes(root)
1270
+ def build_methods_and_def_nodes(root, source_path = nil)
1270
1271
  methods = {}
1271
1272
  def_nodes = {}
1272
- walk_methods_and_def_nodes(root, [], false, methods, def_nodes)
1273
+ walk_methods_and_def_nodes(root, [], false, methods, def_nodes, source_path)
1273
1274
  apply_alias_def_nodes(root, def_nodes)
1274
1275
  [methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
1275
1276
  end
1276
1277
 
1277
1278
  # 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).
1279
+ # table and the current file's table combine instead of clobbering). A name recorded on both sides — an instance
1280
+ # `def` here and a `class << self` twin there — merges to {Scope::DiscoveryIndex::METHOD_KIND_BOTH} rather than
1281
+ # letting the overlay's kind win (#239).
1279
1282
  def deep_merge_class_methods(base, overlay)
1280
1283
  return overlay if base.nil? || base.empty?
1281
1284
  return base if overlay.empty?
1282
1285
 
1283
1286
  base.merge(overlay) do |_class_name, base_methods, overlay_methods|
1284
- base_methods.merge(overlay_methods)
1287
+ merge_method_kinds(base_methods, overlay_methods)
1285
1288
  end
1286
1289
  end
1287
1290
 
1291
+ # `{ method => kind }` union that promotes a kind disagreement to `METHOD_KIND_BOTH` instead of clobbering.
1292
+ def merge_method_kinds(base_methods, overlay_methods)
1293
+ base_methods.merge(overlay_methods) do |_method_name, base_kind, overlay_kind|
1294
+ base_kind == overlay_kind ? base_kind : Scope::DiscoveryIndex::METHOD_KIND_BOTH
1295
+ end
1296
+ end
1297
+
1298
+ # The single write path into a `class_name => { method => kind }` existence table. Every recorder goes through
1299
+ # it so a class that defines one name on both sides keeps both facts: the table is keyed by name alone, so a
1300
+ # bare assignment silently replaced the other side's kind and `Scope#discovered_method?` then answered false for
1301
+ # a method the source plainly defines (#239).
1302
+ def record_method_kind(accumulator, class_name, method_name, kind)
1303
+ table = (accumulator[class_name] ||= {})
1304
+ recorded = table[method_name]
1305
+ table[method_name] =
1306
+ if recorded.nil? || recorded == kind
1307
+ kind
1308
+ else
1309
+ Scope::DiscoveryIndex::METHOD_KIND_BOTH
1310
+ end
1311
+ end
1312
+
1288
1313
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
1289
1314
  # Combined `walk_methods` + `walk_def_nodes` descent. The two walks had identical class / module / singleton-class
1290
1315
  # / meta-block traversals and both stopped at `DefNode`; the only divergences are leaf actions (recorded into the
1291
1316
  # right accumulator) and the original `walk_methods` returning at `AliasMethodNode` (its symbol-only children
1292
1317
  # carry no def / class node, so not descending them is byte-identical for `def_nodes` too). See
1293
1318
  # {#build_methods_and_def_nodes}.
1294
- def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1319
+ def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc,
1320
+ source_path = nil)
1295
1321
  return unless node.is_a?(Prism::Node)
1296
1322
 
1297
1323
  case node
@@ -1300,21 +1326,27 @@ module Rigor
1300
1326
  if name
1301
1327
  child_prefix = qualified_prefix + [name]
1302
1328
  record_meta_superclass_members(node, child_prefix, methods_acc) if node.is_a?(Prism::ClassNode)
1303
- walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc) if node.body
1329
+ if node.body
1330
+ walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc, source_path)
1331
+ end
1304
1332
  return
1305
1333
  end
1306
1334
  when Prism::SingletonClassNode
1307
1335
  if node.body
1308
1336
  singleton_prefix = singleton_class_prefix(node, qualified_prefix)
1309
1337
  if singleton_prefix
1310
- walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc)
1338
+ walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc, source_path)
1311
1339
  return
1312
1340
  end
1313
1341
  end
1314
1342
  when Prism::ConstantWriteNode
1315
1343
  if meta_new_block_body(node)
1316
1344
  child_prefix = qualified_prefix + [node.name.to_s]
1317
- walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc)
1345
+ walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc,
1346
+ source_path)
1347
+ # No anonymous registration here: the constant IS the name, and `StatementEvaluator` never routes a
1348
+ # constant-write rvalue through the block-body narrowing (its scope index still shows `self` as
1349
+ # `Dynamic[top]` inside such a body), so the two passes agree on the constant name alone.
1318
1350
  return
1319
1351
  end
1320
1352
  when Prism::DefNode
@@ -1325,14 +1357,43 @@ module Rigor
1325
1357
  record_alias_method(node, qualified_prefix, in_singleton_class, methods_acc)
1326
1358
  return
1327
1359
  when Prism::CallNode
1328
- record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
1329
- if ATTR_MACROS.include?(node.name)
1330
- record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc)
1360
+ anonymous = record_call_node_methods(node, qualified_prefix, in_singleton_class, methods_acc, source_path)
1361
+ if anonymous
1362
+ walk_anonymous_meta_block(node, anonymous, qualified_prefix, in_singleton_class, methods_acc,
1363
+ def_nodes_acc, source_path)
1364
+ return
1331
1365
  end
1332
1366
  end
1333
1367
 
1334
1368
  node.rigor_each_child do |child|
1335
- walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1369
+ walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc,
1370
+ source_path)
1371
+ end
1372
+ end
1373
+
1374
+ # The `Prism::CallNode` leaf actions of {#walk_methods_and_def_nodes}: the `define_method` / `attr_*` macro
1375
+ # recorders, plus the {AnonymousMetaClass} name of a class-creating meta call carrying a block (nil for
1376
+ # every other call), which the caller uses to decide whether the block body needs the anonymous-class-body
1377
+ # descent.
1378
+ def record_call_node_methods(node, qualified_prefix, in_singleton_class, methods_acc, source_path)
1379
+ record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
1380
+ record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if ATTR_MACROS.include?(node.name)
1381
+ AnonymousMetaClass.name_for(node, source_path)
1382
+ end
1383
+
1384
+ # #319 — walks a `Class.new do ... end` / `Module.new do ... end` / `Struct.new(*sym) do ... end` /
1385
+ # `Data.define(*sym) do ... end` block body as the class body it is at runtime, keyed by the call site's
1386
+ # synthetic anonymous `name`; the call's other children (receiver, arguments) keep the enclosing prefix.
1387
+ def walk_anonymous_meta_block(call_node, name, qualified_prefix, in_singleton_class, methods_acc,
1388
+ def_nodes_acc, source_path)
1389
+ call_node.rigor_each_child do |child|
1390
+ if child.equal?(call_node.block)
1391
+ body = call_node.block.body
1392
+ walk_methods_and_def_nodes(body, [name], false, methods_acc, def_nodes_acc, source_path) if body
1393
+ else
1394
+ walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc,
1395
+ source_path)
1396
+ end
1336
1397
  end
1337
1398
  end
1338
1399
 
@@ -1342,21 +1403,38 @@ module Rigor
1342
1403
  # (semantically `class << self`).
1343
1404
  # - `class << Foo` not nested in `class Foo` returns `[Foo]`
1344
1405
  # so methods defined inside register on Foo's singleton.
1406
+ # - `class << Foo = <expr>` (#320, the private-singleton-object
1407
+ # idiom) is the same case: Ruby evaluates the assignment, then
1408
+ # opens the singleton of the resulting object — which is the
1409
+ # object `Foo` now holds — so the body's methods are reachable
1410
+ # as `Foo.<name>` exactly as for a plain constant read.
1345
1411
  # - Any other expression (variable, method call) returns nil
1346
1412
  # so the walker falls through and skips the body.
1347
1413
  def singleton_class_prefix(node, qualified_prefix)
1348
- case node.expression
1349
- when Prism::SelfNode
1414
+ return qualified_prefix if node.expression.is_a?(Prism::SelfNode)
1415
+
1416
+ rendered = singleton_receiver_constant_name(node.expression)
1417
+ return nil unless rendered
1418
+
1419
+ if !qualified_prefix.empty? && qualified_prefix.last == rendered
1350
1420
  qualified_prefix
1351
- when Prism::ConstantReadNode, Prism::ConstantPathNode
1352
- rendered = Source::ConstantPath.qualified_name(node.expression)
1353
- return nil unless rendered
1421
+ else
1422
+ rendered.split("::")
1423
+ end
1424
+ end
1354
1425
 
1355
- if !qualified_prefix.empty? && qualified_prefix.last == rendered
1356
- qualified_prefix
1357
- else
1358
- rendered.split("::")
1359
- end
1426
+ # The constant a `class << X` operand names, or nil when the operand is not constant-shaped. Both the
1427
+ # read spellings (`Foo`, `A::Foo`, `::Foo`) and the two constant-*write* spellings (`Foo = expr`,
1428
+ # `A::Foo = expr`) resolve to the same unqualified rendering the read branch uses, so a body opened on
1429
+ # the assignment and one opened on a later plain read land on the same table key.
1430
+ def singleton_receiver_constant_name(expression)
1431
+ case expression
1432
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
1433
+ Source::ConstantPath.qualified_name(expression)
1434
+ when Prism::ConstantWriteNode
1435
+ expression.name.to_s
1436
+ when Prism::ConstantPathWriteNode
1437
+ Source::ConstantPath.qualified_name(expression.target)
1360
1438
  end
1361
1439
  end
1362
1440
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
@@ -1393,8 +1471,7 @@ module Rigor
1393
1471
  return if members.empty?
1394
1472
 
1395
1473
  class_name = qualified_prefix.join("::")
1396
- table = (accumulator[class_name] ||= {})
1397
- members.each { |member| table[member] ||= :instance }
1474
+ members.each { |member| record_method_kind(accumulator, class_name, member, :instance) }
1398
1475
  end
1399
1476
 
1400
1477
  # The Symbol member names of a `Data.define(*Symbol)` / `Struct.new(*Symbol [, keyword_init:])` call. For
@@ -1412,8 +1489,7 @@ module Rigor
1412
1489
  class_name = qualified_prefix.join("::")
1413
1490
  singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
1414
1491
  kind = singleton ? :singleton : :instance
1415
- accumulator[class_name] ||= {}
1416
- accumulator[class_name][def_node.name] = kind
1492
+ record_method_kind(accumulator, class_name, def_node.name, kind)
1417
1493
  end
1418
1494
 
1419
1495
  # `def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) is semantically equivalent to `def
@@ -1461,6 +1537,24 @@ module Rigor
1461
1537
  class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
1462
1538
  accumulator[class_name] ||= {}
1463
1539
  accumulator[class_name][def_node.name] = def_node
1540
+ record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator)
1541
+ end
1542
+
1543
+ # #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
1544
+ # Before the anonymous class had a name the body was walked with an empty prefix, so every such `def`
1545
+ # landed there; that is the same leniency this key already grants a `def` nested in any other DSL block
1546
+ # (see {TOP_LEVEL_DEF_KEY}), and it is what lets an implicit-self call elsewhere in the file resolve
1547
+ # against a method the anonymous module contributes to some other object's `self` — the
1548
+ # `Module.new { def start; end }` mixed into a spawned actor environment, then called from the sibling
1549
+ # `spawn(...) { start }` block. Giving the body a class of its own must not silently retract it: the call
1550
+ # resolves at runtime, and `call.unresolved-toplevel` firing on it would be a new false positive traded
1551
+ # for the ones this change retires. Never clobbers a real top-level `def` of the same name.
1552
+ def record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator)
1553
+ return unless qualified_prefix.length == 1
1554
+ return unless Type::AnonymousClassName.match?(qualified_prefix.first)
1555
+
1556
+ table = (accumulator[TOP_LEVEL_DEF_KEY] ||= {})
1557
+ table[def_node.name] ||= def_node
1464
1558
  end
1465
1559
 
1466
1560
  # Module-singleton call resolution (ADR-57 follow-up) — the SINGLETON-side mirror of `build_discovered_def_nodes`.
@@ -1595,16 +1689,18 @@ module Rigor
1595
1689
  # `class Foo < Bar` declaration. Only constant superclasses are recorded (`class Foo < Struct.new(...)` and other
1596
1690
  # non-constant superclasses produce no entry). The as-written name is resolved to a qualified class at the call
1597
1691
  # site against the subclass's lexical nesting — see `ExpressionTyper#resolve_ancestor_class_name`.
1598
- def build_discovered_superclasses(root)
1692
+ def build_discovered_superclasses(root, source_path = nil)
1599
1693
  accumulator = {}
1600
- walk_class_superclasses(root, [], accumulator)
1694
+ walk_class_superclasses(root, [], accumulator, source_path)
1601
1695
  accumulator.freeze
1602
1696
  end
1603
1697
 
1604
- def walk_class_superclasses(node, qualified_prefix, accumulator)
1698
+ def walk_class_superclasses(node, qualified_prefix, accumulator, source_path = nil)
1605
1699
  return unless node.is_a?(Prism::Node)
1606
1700
 
1607
1701
  case node
1702
+ when Prism::CallNode
1703
+ record_anonymous_meta_superclass(node, accumulator, source_path)
1608
1704
  when Prism::ClassNode
1609
1705
  name = Source::ConstantPath.qualified_name(node.constant_path)
1610
1706
  if name
@@ -1623,10 +1719,27 @@ module Rigor
1623
1719
  end
1624
1720
 
1625
1721
  node.rigor_each_child do |child|
1626
- walk_class_superclasses(child, qualified_prefix, accumulator)
1722
+ walk_class_superclasses(child, qualified_prefix, accumulator, source_path)
1627
1723
  end
1628
1724
  end
1629
1725
 
1726
+ # #319 — `Class.new(Parent) do ... end` names its superclass in the first positional. Recording it under the
1727
+ # call site's anonymous name keeps `Parent`'s surface reachable from the block body (whose `self_type` is now
1728
+ # `Singleton[<anonymous>]`) and from an instance of the resulting class, so giving the anonymous class an
1729
+ # identity does not cost the inheritance the old `Singleton[Parent]` answer carried for free.
1730
+ def record_anonymous_meta_superclass(call_node, accumulator, source_path)
1731
+ return unless AnonymousMetaClass.block_form_receiver(call_node) == :Class
1732
+
1733
+ arg = call_node.arguments&.arguments&.first
1734
+ return if arg.nil?
1735
+
1736
+ superclass = Source::ConstantPath.qualified_name(arg)
1737
+ return if superclass.nil?
1738
+
1739
+ name = AnonymousMetaClass.name_for(call_node, source_path)
1740
+ accumulator[name] = superclass if name
1741
+ end
1742
+
1630
1743
  # ADR-48 — per qualified class name -> ordered `Data.define` member-name list, for both the named-subclass form
1631
1744
  # (`class Point < Data.define(:x, :y)`) and the constant-assigned form (`Point = Data.define(:x, :y)`). Only
1632
1745
  # `Data.define` is recorded: `Struct.new` instances are mutable, so member-value folding would be unsound (the
@@ -1918,7 +2031,7 @@ module Rigor
1918
2031
  class_name = qualified_prefix.join("::")
1919
2032
  new_name = alias_node.new_name.unescaped.to_sym
1920
2033
  kind = in_singleton_class ? :singleton : :instance
1921
- (accumulator[class_name] ||= {})[new_name] = kind
2034
+ record_method_kind(accumulator, class_name, new_name, kind)
1922
2035
  end
1923
2036
 
1924
2037
  # Post-pass over the `def_nodes` accumulator: for every `alias` declaration inside a class body, if the original
@@ -1981,8 +2094,7 @@ module Rigor
1981
2094
  return if method_name.nil?
1982
2095
 
1983
2096
  class_name = qualified_prefix.join("::")
1984
- accumulator[class_name] ||= {}
1985
- accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance
2097
+ record_method_kind(accumulator, class_name, method_name, in_singleton_class ? :singleton : :instance)
1986
2098
  end
1987
2099
 
1988
2100
  # The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
@@ -2005,9 +2117,8 @@ module Rigor
2005
2117
  base = literal_method_name(arg)
2006
2118
  next if base.nil?
2007
2119
 
2008
- accumulator[class_name] ||= {}
2009
- accumulator[class_name][base] = kind if reader
2010
- accumulator[class_name][:"#{base}="] = kind if writer
2120
+ record_method_kind(accumulator, class_name, base, kind) if reader
2121
+ record_method_kind(accumulator, class_name, :"#{base}=", kind) if writer
2011
2122
  end
2012
2123
  end
2013
2124
 
@@ -2352,7 +2463,7 @@ module Rigor
2352
2463
  file_index[:def_nodes].each { |cn, methods| (acc[:def_nodes][cn] ||= {}).merge!(methods) }
2353
2464
  file_index[:singleton_def_nodes].each { |cn, methods| (acc[:singleton_def_nodes][cn] ||= {}).merge!(methods) }
2354
2465
  file_index[:method_visibilities].each { |cn, table| (acc[:method_visibilities][cn] ||= {}).merge!(table) }
2355
- file_index[:methods].each { |cn, table| (acc[:methods][cn] ||= {}).merge!(table) }
2466
+ file_index[:methods].each { |cn, table| acc[:methods][cn] = merge_method_kinds(acc[:methods][cn] || {}, table) }
2356
2467
  fold_def_sources(acc, :def_sources, file_index[:def_sources])
2357
2468
  fold_def_sources(acc, :singleton_def_sources, file_index[:singleton_def_sources])
2358
2469
  end
@@ -2468,10 +2579,18 @@ module Rigor
2468
2579
 
2469
2580
  # Removes, per class, the method names that have a project `def` node, leaving only
2470
2581
  # accessor/alias/define_method-introduced methods in the cross-file suppression table.
2582
+ #
2583
+ # `def_nodes` is the INSTANCE-side table, so a name recorded on both sides keeps its singleton half: that half
2584
+ # comes from a `def self.x` / `class << self` definition this rule never covered, and dropping it whole would
2585
+ # reintroduce #239's false `call.undefined-method` across files.
2471
2586
  def subtract_def_methods(methods, def_nodes)
2472
2587
  methods.each_with_object({}) do |(class_name, table), out|
2473
2588
  defs = def_nodes[class_name] || {}
2474
- kept = table.reject { |method_name, _kind| defs.key?(method_name) }
2589
+ kept = table.each_with_object({}) do |(method_name, kind), acc|
2590
+ next acc[method_name] = kind unless defs.key?(method_name)
2591
+
2592
+ acc[method_name] = :singleton if kind == Scope::DiscoveryIndex::METHOD_KIND_BOTH
2593
+ end
2475
2594
  out[class_name] = kept unless kept.empty?
2476
2595
  end
2477
2596
  end
@@ -2483,7 +2602,7 @@ module Rigor
2483
2602
  # One combined descent yields both the methods existence table and the def-node table; the latter is also
2484
2603
  # consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
2485
2604
  # def-nodes ×2). See {#build_methods_and_def_nodes}.
2486
- file_methods, file_def_nodes = build_methods_and_def_nodes(root)
2605
+ file_methods, file_def_nodes = build_methods_and_def_nodes(root, path)
2487
2606
  merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
2488
2607
  # ADR-46 slice 4 (singleton) — record the singleton-side `"path:line"` sources alongside the nodes,
2489
2608
  # the exact mirror of the instance-side `merge_discovered_defs`, so a class/singleton-method body edit
@@ -2491,7 +2610,7 @@ module Rigor
2491
2610
  # silently degrading to the file's full ancestry closure.
2492
2611
  merge_discovered_defs(acc[:singleton_def_nodes], acc[:singleton_def_sources], path,
2493
2612
  build_discovered_singleton_def_nodes(root))
2494
- superclasses = build_discovered_superclasses(root)
2613
+ superclasses = build_discovered_superclasses(root, path)
2495
2614
  includes = build_discovered_includes(root)
2496
2615
  acc[:superclasses].merge!(superclasses)
2497
2616
  includes.each do |class_name, mods|
@@ -2517,7 +2636,7 @@ module Rigor
2517
2636
  (acc[:method_visibilities][class_name] ||= {}).merge!(table)
2518
2637
  end
2519
2638
  file_methods.each do |class_name, table|
2520
- (acc[:methods][class_name] ||= {}).merge!(table)
2639
+ acc[:methods][class_name] = merge_method_kinds(acc[:methods][class_name] || {}, table)
2521
2640
  end
2522
2641
  end
2523
2642
 
@@ -2570,24 +2689,47 @@ module Rigor
2570
2689
  node.rigor_each_child { |child| collect_class_decls(child, qualified_prefix, accumulator) }
2571
2690
  end
2572
2691
 
2573
- # T1 (template-corpora survey) — record a `Const = Class.new(Super)` (and the bare `Class.new` / `Module.new`)
2574
- # class-creating constant in the cross-file discovery table so a reference to `Const` from ANOTHER file under the
2575
- # same namespace resolves to the project class instead of falling through to a core same-named class
2692
+ # T1 (template-corpora survey) — record a class-creating constant write (`Const = Class.new(Super)`, the bare
2693
+ # `Class.new` / `Module.new`, and the `Data.define(*sym)` / `Struct.new(*sym)` forms, each with or without a
2694
+ # block) in the cross-file discovery table so a reference to `Const` from ANOTHER file under the same namespace
2695
+ # resolves to the project class instead of falling through to a same-named class elsewhere
2576
2696
  # (`Liquid::SyntaxError = Class.new(Error)` referenced in a sibling file's `rescue SyntaxError => e`, which
2577
2697
  # 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
- # The superclass name is resolved lexically against the enclosing prefix; a bare `Class.new` with no superclass
2580
- # (or `Module.new`) types as `Singleton[Const]` itself. The block form is left to the existing
2581
- # `meta_new_block_body` machinery only the plain `Class.new(Super)` constant (the namespaced-sibling-error
2582
- # idiom) is added here.
2698
+ # `Class.new(Super)` as `Singleton[Super]` (the constructed class answers method lookups through Super's chain)
2699
+ # and every other recognised form as `Singleton[Const]`.
2700
+ #
2701
+ # #271 — the Data/Struct forms are here because their absence was an ACTIVE false positive, not merely a missed
2702
+ # resolution: a nested `Const = Data.define(...)` invisible cross-file lets Ruby's lexical walk continue to the
2703
+ # PARENT namespace's same-named sibling, so `Analysis::PluginFactFingerprint.from_registry(...).opaque?` typed
2704
+ # its receiver as the unrelated `Rigor::Analysis::Result` and reported `call.undefined-method` on correct code.
2705
+ # Nested `Result` / `Entry` / `Config` Data constants shadowing a sibling are ordinary Ruby, and only the
2706
+ # DEFINING file's `in_source_constants` (never part of the project seed) knew about them.
2583
2707
  def record_class_new_constant_decl(node, qualified_prefix, accumulator)
2584
2708
  rvalue = node.value
2585
- return unless class_new_call?(rvalue) || module_new_call?(rvalue)
2586
- return if rvalue.block # block form: handled by meta_new_block_body walks
2709
+ return unless meta_new_constant_rvalue?(rvalue)
2587
2710
 
2588
2711
  full = (qualified_prefix + [node.name.to_s]).join("::")
2589
- super_name = class_new_superclass_name(rvalue, qualified_prefix, accumulator)
2590
- accumulator[full] = Type::Combinator.singleton_of(super_name || full)
2712
+ accumulator[full] = Type::Combinator.singleton_of(
2713
+ meta_new_constant_decl_name(rvalue, full, qualified_prefix, accumulator)
2714
+ )
2715
+ end
2716
+
2717
+ # The four recognised class-creating rvalue shapes at constant-write position — the same set
2718
+ # {#meta_new_block_body} recognises, so the cross-file table and the per-file block-as-method walk agree on what
2719
+ # counts as a declaration.
2720
+ def meta_new_constant_rvalue?(rvalue)
2721
+ class_new_call?(rvalue) || module_new_call?(rvalue) ||
2722
+ data_define_call?(rvalue) || struct_new_call?(rvalue)
2723
+ end
2724
+
2725
+ # The name the constant's `Singleton[...]` carries. Only a block-less `Class.new(Super)` borrows its superclass's
2726
+ # name (the constructed class answers lookups through `Super`'s chain and declares nothing of its own); every
2727
+ # other form — Data/Struct members, any block body — owns methods under its OWN qualified name, which is exactly
2728
+ # what the per-file `meta_new_constant_type` answers.
2729
+ def meta_new_constant_decl_name(rvalue, full, qualified_prefix, accumulator)
2730
+ return full if rvalue.block || data_define_call?(rvalue) || struct_new_call?(rvalue)
2731
+
2732
+ class_new_superclass_name(rvalue, qualified_prefix, accumulator) || full
2591
2733
  end
2592
2734
 
2593
2735
  # Lexically-qualified name of a `Class.new(Super)` superclass argument, or nil when there is no positional
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../source/constant_path"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # Issue #320 — the *private-singleton-object* idiom: a constant holds a plain object whose singleton
8
+ # class carries the methods.
9
+ #
10
+ # class << Merger = Object.new
11
+ # def merge_attributes!(a, b) = a
12
+ # end
13
+ # Merger.merge_attributes!({}, {})
14
+ #
15
+ # {ScopeIndexer} already records such a body under the constant's own name with kind `:singleton` — that
16
+ # keying is correct for both spellings, since `Foo.bar` means "call `bar` on the value of constant `Foo`"
17
+ # whether the value is a class object or an ordinary object. The gap is on the *receiver* side: when the
18
+ # constant names a class or module the read types as `Singleton[Foo]` and dispatch recovers the name from
19
+ # the type, but when it holds an ordinary object the read types as `Nominal[Object]` and the name is
20
+ # gone — so every method in the body is invisible and `call.undefined-method` fires on working code.
21
+ #
22
+ # This module recovers the name from the receiver *syntax* instead, for exactly the receivers whose type
23
+ # has lost it. `Singleton` receivers are excluded so the established class/module path keeps its
24
+ # precedence; a receiver whose constant has no recorded singleton body yields nil, so an undefined method
25
+ # on such a constant (`Merger.nope`) still fires.
26
+ module SingletonObjectConstant
27
+ module_function
28
+
29
+ # The constant name a call's receiver names, when that receiver is a constant reference whose type is
30
+ # not already a `Singleton` carrier. Nil for every other receiver shape.
31
+ def receiver_constant_name(call_node, receiver_type)
32
+ return nil if receiver_type.is_a?(Type::Singleton)
33
+
34
+ receiver = call_node.receiver
35
+ return nil unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
36
+
37
+ Source::ConstantPath.qualified_name(receiver)
38
+ end
39
+
40
+ # True when the project recorded `method_name` on the singleton side of the constant this call's
41
+ # receiver names. The suppression probe for `call.undefined-method`.
42
+ def recorded?(call_node, receiver_type, method_name, scope)
43
+ return false if scope.nil?
44
+
45
+ name = receiver_constant_name(call_node, receiver_type)
46
+ return false if name.nil?
47
+
48
+ scope.discovered_method?(name, method_name, :singleton)
49
+ end
50
+
51
+ # The `Prism::DefNode` for `method_name` in the constant's singleton body, or nil. The inference tier's
52
+ # entry point.
53
+ def def_node_for(call_node, receiver_type, method_name, scope)
54
+ return nil if scope.nil?
55
+
56
+ name = receiver_constant_name(call_node, receiver_type)
57
+ return nil if name.nil?
58
+
59
+ scope.singleton_def_for(name, method_name)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -8,6 +8,7 @@ require_relative "../analysis/fact_store"
8
8
  require_relative "../source/node_walker"
9
9
  require_relative "../source/node_children"
10
10
  require_relative "../source/constant_path"
11
+ require_relative "anonymous_meta_class"
11
12
  require_relative "block_parameter_binder"
12
13
  require_relative "body_fixpoint"
13
14
  require_relative "dynamic_origin"
@@ -20,6 +21,7 @@ require_relative "method_parameter_binder"
20
21
  require_relative "multi_target_binder"
21
22
  require_relative "mutation_widening"
22
23
  require_relative "narrowing"
24
+ require_relative "optimistic_origin"
23
25
 
24
26
  module Rigor
25
27
  module Inference
@@ -247,9 +249,25 @@ module Rigor
247
249
 
248
250
  bound = bound.without_inferred_param_mark(node.name)
249
251
  bound = bound.with_local_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
252
+ bound = bound.with_optimistic_local(node.name, optimistic_rhs_origin(node.value, post_rhs))
250
253
  [rhs_type, bound]
251
254
  end
252
255
 
256
+ # Issue #286 — the optimistic-nil-free counterpart of {#rhs_origin}, differing in two ways. It does not
257
+ # gate on `Dynamic`: the values this channel marks are ordinary `Union` / `Constant` / `Nominal`
258
+ # carriers, which is the whole point. And it resolves a bare local read through its binding, so
259
+ # `w = v` keeps the mark — the same propagation `OriginLookup` performs for the Dynamic channel.
260
+ def optimistic_rhs_origin(value_node, scope_after_rhs)
261
+ optimistic_origin_for(value_node, scope_after_rhs)
262
+ end
263
+
264
+ # The effective optimistic-nil-free cause of an expression. {Inference::OptimisticOrigin.resolve} owns
265
+ # the judgment — the mark on the node itself, the binding a bare local / ivar read resolves through, and
266
+ # the predicate-fold derivation of issue #313.
267
+ def optimistic_origin_for(node, scope)
268
+ Inference::OptimisticOrigin.resolve(node, scope)
269
+ end
270
+
253
271
  # ADR-82 WD1 — the {Inference::DynamicOrigin} cause to propagate onto a local / ivar being bound to `rhs`.
254
272
  # Returns the cause recorded on the assignment's rhs node when the value is `Dynamic` (so a later
255
273
  # `x` / `@x` receiver-read resolves to why it is dynamic), else `nil` — `with_local_origin` /
@@ -277,6 +295,7 @@ module Rigor
277
295
  rhs_type, post_rhs = sub_eval(node.value, scope)
278
296
  bound = post_rhs.with_ivar(node.name, rhs_type)
279
297
  bound = bound.with_ivar_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
298
+ bound = bound.with_optimistic_ivar(node.name, optimistic_rhs_origin(node.value, post_rhs))
280
299
  [rhs_type, bound]
281
300
  end
282
301
 
@@ -543,19 +562,34 @@ module Rigor
543
562
  # non-falsey carriers like `Nominal[Integer]` (Integer is always truthy in Ruby — including 0) also collapse the
544
563
  # dead else.
545
564
  def live_branch_for_if(node, pred_type, post_pred)
546
- case Narrowing.predicate_certainty(pred_type)
565
+ verdict = optimistic_carrier?(node.predicate, post_pred) ? nil : Narrowing.predicate_certainty(pred_type)
566
+ case verdict
547
567
  when :truthy then eval_branch_or_nil(node.statements, post_pred)
548
568
  when :falsey then eval_branch_or_nil(node.subsequent, post_pred)
549
569
  end
550
570
  end
551
571
 
552
572
  def live_branch_for_unless(node, pred_type, post_pred)
553
- case Narrowing.predicate_certainty(pred_type)
573
+ verdict = optimistic_carrier?(node.predicate, post_pred) ? nil : Narrowing.predicate_certainty(pred_type)
574
+ case verdict
554
575
  when :truthy then eval_branch_or_nil(node.else_clause, post_pred)
555
576
  when :falsey then eval_branch_or_nil(node.statements, post_pred)
556
577
  end
557
578
  end
558
579
 
580
+ # ADR-101 — the branch elision MUST NOT conclude truthiness from a carrier whose nil-freeness rests on
581
+ # the `%a{implicitly-returns-nil}` that `RbsDispatch` reads past. Such a value is optimistic, not proof
582
+ # (see {Inference::OptimisticOrigin} and docs/internal-spec/inference-engine.md), so eliding an arm on
583
+ # it deletes a branch the program really takes when the lookup misses.
584
+ #
585
+ # The decline lives here and NOT in `Narrowing.falsey_nominal?` / `.narrow_falsey`: `&&=` / `||=` and
586
+ # the and/or surviving-left edge read those too, and widening the falsey fragment there would re-admit
587
+ # `nil` into a bound local and buy `possible nil receiver` false positives — a soundness fix paid for
588
+ # in FPs, which is the wrong trade.
589
+ def optimistic_carrier?(predicate, scope)
590
+ !optimistic_origin_for(predicate, scope).nil?
591
+ end
592
+
559
593
  def eval_else(node)
560
594
  return [Type::Combinator.constant_of(nil), scope] if node.statements.nil?
561
595
 
@@ -1789,7 +1823,20 @@ module Rigor
1789
1823
  return unless block.is_a?(Prism::BlockNode)
1790
1824
 
1791
1825
  block_entry = build_block_entry_scope(node, block)
1792
- sub_eval(block, block_entry)
1826
+ # #319 — `Class.new do ... end` and friends evaluate their block as a CLASS BODY (`class_eval`
1827
+ # semantics): `self` is the freshly created class, so a `def` inside defines an instance method on it
1828
+ # and `attr_reader` runs as a class-level macro. Enter the block under the same `self_type` /
1829
+ # class-context a `class Foo ... end` body gets, keyed by the call site's anonymous name — the name
1830
+ # `ScopeIndexer` registered the body's methods under. Without it the body inherits the enclosing
1831
+ # scope, and at file top level that means `Scope#toplevel?` (a nil `self_type`) reports every macro
1832
+ # call in the body as `call.unresolved-toplevel`.
1833
+ #
1834
+ # Outer locals stay visible: unlike a `class` keyword body, the block is a closure.
1835
+ anonymous = AnonymousMetaClass.name_for(node, scope.source_path)
1836
+ return sub_eval(block, block_entry) if anonymous.nil?
1837
+
1838
+ sub_eval(block, block_entry.with_self_type(Type::Combinator.singleton_of(anonymous)),
1839
+ class_context: [ClassFrame.new(name: anonymous, singleton: false)])
1793
1840
  end
1794
1841
 
1795
1842
  # Slice 6 phase C sub-phase 3b/3c. When the call carries a block whose receiving method is NOT proven
@@ -2466,7 +2513,11 @@ module Rigor
2466
2513
  def build_block_entry_scope(call_node, block_node)
2467
2514
  expected = expected_block_param_types_for(call_node)
2468
2515
  bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
2469
- scope_with_params = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
2516
+ # Issue #316 every block body enters with `self` unmodelled (`Scope#entering_opaque_block`); the
2517
+ # yielding method, not the lexical context, decides what `self` is, and Rigor does not track it.
2518
+ scope_with_params = bindings.reduce(scope.entering_opaque_block) do |acc, (name, type)|
2519
+ acc.with_local(name, type)
2520
+ end
2470
2521
  block_local_names(block_node).reduce(scope_with_params) do |acc, name|
2471
2522
  acc.with_local(name, Type::Combinator.constant_of(nil))
2472
2523
  end