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.
- checksums.yaml +4 -4
- data/README.md +10 -10
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +5 -0
- 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/manual/02-cli-reference.md +71 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +8 -0
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +107 -28
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_cache_key.rb +17 -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 +78 -7
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- 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/cli/check_command.rb +49 -28
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- 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/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +28 -13
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +62 -4
- data/lib/rigor/inference/fork_map.rb +47 -4
- 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/rbs_dispatch.rb +158 -12
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +234 -11
- 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/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +123 -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 +197 -55
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +55 -4
- data/lib/rigor/language_server/buffer_table.rb +22 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
- 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 +20 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +190 -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 +114 -10
- 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 +12 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -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/inference/void_origin.rbs +6 -3
- data/sig/rigor/scope.rbs +12 -0
- data/sig/rigor.rbs +4 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
1329
|
-
if
|
|
1330
|
-
|
|
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
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1352
|
-
rendered
|
|
1353
|
-
|
|
1421
|
+
else
|
|
1422
|
+
rendered.split("::")
|
|
1423
|
+
end
|
|
1424
|
+
end
|
|
1354
1425
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2009
|
-
accumulator
|
|
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]
|
|
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.
|
|
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]
|
|
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)
|
|
2574
|
-
#
|
|
2575
|
-
#
|
|
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
|
-
#
|
|
2580
|
-
#
|
|
2581
|
-
#
|
|
2582
|
-
#
|
|
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
|
|
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
|
-
|
|
2590
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|