rigortype 0.3.2 → 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 +1 -1
- data/docs/manual/04-diagnostics.md +8 -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_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +62 -22
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +56 -13
- data/lib/rigor/inference/fork_map.rb +45 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/optimistic_origin.rb +68 -3
- data/lib/rigor/inference/scope_indexer.rb +119 -28
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +25 -12
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
- data/lib/rigor/scope.rb +62 -2
- 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/sig/rigor/scope.rbs +4 -0
- data/sig/rigor.rbs +4 -0
- metadata +5 -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,10 +1267,10 @@ 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
|
|
@@ -1315,7 +1316,8 @@ module Rigor
|
|
|
1315
1316
|
# right accumulator) and the original `walk_methods` returning at `AliasMethodNode` (its symbol-only children
|
|
1316
1317
|
# carry no def / class node, so not descending them is byte-identical for `def_nodes` too). See
|
|
1317
1318
|
# {#build_methods_and_def_nodes}.
|
|
1318
|
-
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)
|
|
1319
1321
|
return unless node.is_a?(Prism::Node)
|
|
1320
1322
|
|
|
1321
1323
|
case node
|
|
@@ -1324,21 +1326,27 @@ module Rigor
|
|
|
1324
1326
|
if name
|
|
1325
1327
|
child_prefix = qualified_prefix + [name]
|
|
1326
1328
|
record_meta_superclass_members(node, child_prefix, methods_acc) if node.is_a?(Prism::ClassNode)
|
|
1327
|
-
|
|
1329
|
+
if node.body
|
|
1330
|
+
walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc, source_path)
|
|
1331
|
+
end
|
|
1328
1332
|
return
|
|
1329
1333
|
end
|
|
1330
1334
|
when Prism::SingletonClassNode
|
|
1331
1335
|
if node.body
|
|
1332
1336
|
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
1333
1337
|
if singleton_prefix
|
|
1334
|
-
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)
|
|
1335
1339
|
return
|
|
1336
1340
|
end
|
|
1337
1341
|
end
|
|
1338
1342
|
when Prism::ConstantWriteNode
|
|
1339
1343
|
if meta_new_block_body(node)
|
|
1340
1344
|
child_prefix = qualified_prefix + [node.name.to_s]
|
|
1341
|
-
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.
|
|
1342
1350
|
return
|
|
1343
1351
|
end
|
|
1344
1352
|
when Prism::DefNode
|
|
@@ -1349,14 +1357,43 @@ module Rigor
|
|
|
1349
1357
|
record_alias_method(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
1350
1358
|
return
|
|
1351
1359
|
when Prism::CallNode
|
|
1352
|
-
|
|
1353
|
-
if
|
|
1354
|
-
|
|
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
|
|
1355
1365
|
end
|
|
1356
1366
|
end
|
|
1357
1367
|
|
|
1358
1368
|
node.rigor_each_child do |child|
|
|
1359
|
-
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
|
|
1360
1397
|
end
|
|
1361
1398
|
end
|
|
1362
1399
|
|
|
@@ -1366,21 +1403,38 @@ module Rigor
|
|
|
1366
1403
|
# (semantically `class << self`).
|
|
1367
1404
|
# - `class << Foo` not nested in `class Foo` returns `[Foo]`
|
|
1368
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.
|
|
1369
1411
|
# - Any other expression (variable, method call) returns nil
|
|
1370
1412
|
# so the walker falls through and skips the body.
|
|
1371
1413
|
def singleton_class_prefix(node, qualified_prefix)
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
|
1374
1420
|
qualified_prefix
|
|
1375
|
-
|
|
1376
|
-
rendered
|
|
1377
|
-
|
|
1421
|
+
else
|
|
1422
|
+
rendered.split("::")
|
|
1423
|
+
end
|
|
1424
|
+
end
|
|
1378
1425
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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)
|
|
1384
1438
|
end
|
|
1385
1439
|
end
|
|
1386
1440
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
@@ -1483,6 +1537,24 @@ module Rigor
|
|
|
1483
1537
|
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
1484
1538
|
accumulator[class_name] ||= {}
|
|
1485
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
|
|
1486
1558
|
end
|
|
1487
1559
|
|
|
1488
1560
|
# Module-singleton call resolution (ADR-57 follow-up) — the SINGLETON-side mirror of `build_discovered_def_nodes`.
|
|
@@ -1617,16 +1689,18 @@ module Rigor
|
|
|
1617
1689
|
# `class Foo < Bar` declaration. Only constant superclasses are recorded (`class Foo < Struct.new(...)` and other
|
|
1618
1690
|
# non-constant superclasses produce no entry). The as-written name is resolved to a qualified class at the call
|
|
1619
1691
|
# site against the subclass's lexical nesting — see `ExpressionTyper#resolve_ancestor_class_name`.
|
|
1620
|
-
def build_discovered_superclasses(root)
|
|
1692
|
+
def build_discovered_superclasses(root, source_path = nil)
|
|
1621
1693
|
accumulator = {}
|
|
1622
|
-
walk_class_superclasses(root, [], accumulator)
|
|
1694
|
+
walk_class_superclasses(root, [], accumulator, source_path)
|
|
1623
1695
|
accumulator.freeze
|
|
1624
1696
|
end
|
|
1625
1697
|
|
|
1626
|
-
def walk_class_superclasses(node, qualified_prefix, accumulator)
|
|
1698
|
+
def walk_class_superclasses(node, qualified_prefix, accumulator, source_path = nil)
|
|
1627
1699
|
return unless node.is_a?(Prism::Node)
|
|
1628
1700
|
|
|
1629
1701
|
case node
|
|
1702
|
+
when Prism::CallNode
|
|
1703
|
+
record_anonymous_meta_superclass(node, accumulator, source_path)
|
|
1630
1704
|
when Prism::ClassNode
|
|
1631
1705
|
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1632
1706
|
if name
|
|
@@ -1645,10 +1719,27 @@ module Rigor
|
|
|
1645
1719
|
end
|
|
1646
1720
|
|
|
1647
1721
|
node.rigor_each_child do |child|
|
|
1648
|
-
walk_class_superclasses(child, qualified_prefix, accumulator)
|
|
1722
|
+
walk_class_superclasses(child, qualified_prefix, accumulator, source_path)
|
|
1649
1723
|
end
|
|
1650
1724
|
end
|
|
1651
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
|
+
|
|
1652
1743
|
# ADR-48 — per qualified class name -> ordered `Data.define` member-name list, for both the named-subclass form
|
|
1653
1744
|
# (`class Point < Data.define(:x, :y)`) and the constant-assigned form (`Point = Data.define(:x, :y)`). Only
|
|
1654
1745
|
# `Data.define` is recorded: `Struct.new` instances are mutable, so member-value folding would be unsound (the
|
|
@@ -2511,7 +2602,7 @@ module Rigor
|
|
|
2511
2602
|
# One combined descent yields both the methods existence table and the def-node table; the latter is also
|
|
2512
2603
|
# consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
|
|
2513
2604
|
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
2514
|
-
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
2605
|
+
file_methods, file_def_nodes = build_methods_and_def_nodes(root, path)
|
|
2515
2606
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
2516
2607
|
# ADR-46 slice 4 (singleton) — record the singleton-side `"path:line"` sources alongside the nodes,
|
|
2517
2608
|
# the exact mirror of the instance-side `merge_discovered_defs`, so a class/singleton-method body edit
|
|
@@ -2519,7 +2610,7 @@ module Rigor
|
|
|
2519
2610
|
# silently degrading to the file's full ancestry closure.
|
|
2520
2611
|
merge_discovered_defs(acc[:singleton_def_nodes], acc[:singleton_def_sources], path,
|
|
2521
2612
|
build_discovered_singleton_def_nodes(root))
|
|
2522
|
-
superclasses = build_discovered_superclasses(root)
|
|
2613
|
+
superclasses = build_discovered_superclasses(root, path)
|
|
2523
2614
|
includes = build_discovered_includes(root)
|
|
2524
2615
|
acc[:superclasses].merge!(superclasses)
|
|
2525
2616
|
includes.each do |class_name, mods|
|
|
@@ -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
|
|
@@ -259,17 +261,11 @@ module Rigor
|
|
|
259
261
|
optimistic_origin_for(value_node, scope_after_rhs)
|
|
260
262
|
end
|
|
261
263
|
|
|
262
|
-
# The effective optimistic-nil-free cause of an expression
|
|
263
|
-
#
|
|
264
|
-
#
|
|
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.
|
|
265
267
|
def optimistic_origin_for(node, scope)
|
|
266
|
-
|
|
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
|
|
268
|
+
Inference::OptimisticOrigin.resolve(node, scope)
|
|
273
269
|
end
|
|
274
270
|
|
|
275
271
|
# ADR-82 WD1 — the {Inference::DynamicOrigin} cause to propagate onto a local / ivar being bound to `rhs`.
|
|
@@ -1827,7 +1823,20 @@ module Rigor
|
|
|
1827
1823
|
return unless block.is_a?(Prism::BlockNode)
|
|
1828
1824
|
|
|
1829
1825
|
block_entry = build_block_entry_scope(node, block)
|
|
1830
|
-
|
|
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)])
|
|
1831
1840
|
end
|
|
1832
1841
|
|
|
1833
1842
|
# Slice 6 phase C sub-phase 3b/3c. When the call carries a block whose receiving method is NOT proven
|
|
@@ -2504,7 +2513,11 @@ module Rigor
|
|
|
2504
2513
|
def build_block_entry_scope(call_node, block_node)
|
|
2505
2514
|
expected = expected_block_param_types_for(call_node)
|
|
2506
2515
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
|
|
2507
|
-
|
|
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
|
|
2508
2521
|
block_local_names(block_node).reduce(scope_with_params) do |acc, name|
|
|
2509
2522
|
acc.with_local(name, Type::Combinator.constant_of(nil))
|
|
2510
2523
|
end
|
|
@@ -92,6 +92,12 @@ module Rigor
|
|
|
92
92
|
dispatch_plugins(node, ancestors, path, scope, states)
|
|
93
93
|
collector_driver&.visit(node, context)
|
|
94
94
|
|
|
95
|
+
# Issue #318 — `defined?`'s operand is never evaluated, so nothing under a `Prism::DefinedNode` is
|
|
96
|
+
# reachable code. Dispatching plugin rules / the collector driver against the DefinedNode itself is
|
|
97
|
+
# fine; descending into `#value` would feed both plugin node_rules and the built-in collectors
|
|
98
|
+
# source that can never run.
|
|
99
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
100
|
+
|
|
95
101
|
child_context = collector_driver&.descend(node, context)
|
|
96
102
|
ancestors.push(node)
|
|
97
103
|
node.rigor_each_child do |child|
|
|
@@ -4,6 +4,7 @@ require "tempfile"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../analysis/buffer_binding"
|
|
6
6
|
require_relative "../analysis/runner"
|
|
7
|
+
require_relative "../inference/fork_map"
|
|
7
8
|
require_relative "diagnostic_oracle"
|
|
8
9
|
require_relative "discovery_seed"
|
|
9
10
|
require_relative "kill_signature"
|
|
@@ -111,6 +112,18 @@ module Rigor
|
|
|
111
112
|
[path, *(@dependents[path] || [])]
|
|
112
113
|
end
|
|
113
114
|
|
|
115
|
+
# Release the process-private mutant file now, instead of waiting for `Tempfile`'s finalizer at process
|
|
116
|
+
# exit. A one-shot `rigor coverage` run does not need this — the process ends and the file goes with it —
|
|
117
|
+
# but a caller that outlives the oracle does: the spec suite's `after(:suite)` residue check (issue #330)
|
|
118
|
+
# runs while the process is still alive, so an un-released file reads as a leak there even though it
|
|
119
|
+
# would have been reclaimed a moment later. Idempotent, and safe on an oracle that never mutated
|
|
120
|
+
# anything (the file is created lazily).
|
|
121
|
+
def release!
|
|
122
|
+
@mutant_file&.close!
|
|
123
|
+
@mutant_file = nil
|
|
124
|
+
@mutant_pid = nil
|
|
125
|
+
end
|
|
126
|
+
|
|
114
127
|
private
|
|
115
128
|
|
|
116
129
|
# The diagnostic signatures the dependents of `path` report while `source` stands in for it. Empty (and
|
|
@@ -136,11 +149,17 @@ module Rigor
|
|
|
136
149
|
|
|
137
150
|
# The process-private mutant file. Created lazily, and re-created after a fork ({CLI::MutationForkScan}
|
|
138
151
|
# workers must not share one path), which the pid guard detects.
|
|
152
|
+
#
|
|
153
|
+
# A worker's copy goes in {Inference::ForkMap.child_scratch_dir} rather than the system temp dir: the
|
|
154
|
+
# worker ends at `exit!`, which runs neither `at_exit` nor `Tempfile`'s finalizer, so a file placed
|
|
155
|
+
# anywhere else survives the run with nobody left who knows its name (issue #330). On the parent — and
|
|
156
|
+
# on the sequential path — that reader answers `nil`, which is `Tempfile.new`'s own default, and the
|
|
157
|
+
# finalizer reclaims the file at normal exit as before.
|
|
139
158
|
def mutant_file
|
|
140
159
|
return @mutant_file if @mutant_file && @mutant_pid == Process.pid
|
|
141
160
|
|
|
142
161
|
@mutant_pid = Process.pid
|
|
143
|
-
@mutant_file = Tempfile.new(["rigor-mutant-", ".rb"])
|
|
162
|
+
@mutant_file = Tempfile.new(["rigor-mutant-", ".rb"], Inference::ForkMap.child_scratch_dir)
|
|
144
163
|
end
|
|
145
164
|
|
|
146
165
|
def seed_for(buffer)
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -23,6 +23,7 @@ module Rigor
|
|
|
23
23
|
:indexed_narrowings, :method_chain_narrowings,
|
|
24
24
|
:declaration_sourced,
|
|
25
25
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
26
|
+
:opaque_block_self,
|
|
26
27
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
27
28
|
:void_origins,
|
|
28
29
|
:optimistic_origins, :optimistic_locals, :optimistic_ivars
|
|
@@ -147,6 +148,7 @@ module Rigor
|
|
|
147
148
|
declaration_sourced: EMPTY_DECLARATION_SOURCED,
|
|
148
149
|
source_path: nil,
|
|
149
150
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
151
|
+
opaque_block_self: false,
|
|
150
152
|
dynamic_origins: {}.compare_by_identity,
|
|
151
153
|
local_origins: EMPTY_ORIGINS,
|
|
152
154
|
ivar_origins: EMPTY_ORIGINS,
|
|
@@ -168,6 +170,7 @@ module Rigor
|
|
|
168
170
|
@declaration_sourced = declaration_sourced
|
|
169
171
|
@source_path = source_path
|
|
170
172
|
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
173
|
+
@opaque_block_self = opaque_block_self
|
|
171
174
|
@dynamic_origins = dynamic_origins
|
|
172
175
|
@local_origins = local_origins
|
|
173
176
|
@ivar_origins = ivar_origins
|
|
@@ -274,6 +277,21 @@ module Rigor
|
|
|
274
277
|
rebuild(struct_fold_safe_locals: locals)
|
|
275
278
|
end
|
|
276
279
|
|
|
280
|
+
# Issue #316 — marks a block body whose `self` Rigor does not model. Ruby gives a block no `self` of its
|
|
281
|
+
# own: the yielding method decides, and `instance_eval` / `instance_exec` (the mechanism behind every
|
|
282
|
+
# `self`-rebinding DSL — RSpec example groups, `Class.new { … }`, Rake, Sinatra) is indistinguishable from
|
|
283
|
+
# `Array#each` without knowing the callee. The flag is set at every block entry that leaves `self_type`
|
|
284
|
+
# unnarrowed and is inherited by every scope derived inside the block; it never leaks past the block,
|
|
285
|
+
# because `eval_call` returns the caller's scope unchanged.
|
|
286
|
+
def entering_opaque_block
|
|
287
|
+
return self if @opaque_block_self
|
|
288
|
+
|
|
289
|
+
rebuild(opaque_block_self: true)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
|
|
293
|
+
def opaque_block_self? = @opaque_block_self
|
|
294
|
+
|
|
277
295
|
# True when `name`'s `Struct` member reads are fold-safe in this body (the local is provably never mutated /
|
|
278
296
|
# aliased / escaped).
|
|
279
297
|
def struct_fold_safe?(name)
|
|
@@ -352,8 +370,11 @@ module Rigor
|
|
|
352
370
|
# whose type is an open-call-site *lower bound* — firing against a lower bound is a false positive by
|
|
353
371
|
# construction (the ADR-67 WD1 reasoning at the parameter boundary, carried one hop into the body). The
|
|
354
372
|
# distinct kind keeps the inferred-param sites separable from ADR-58's ivar-copy `:local` mark, which a
|
|
355
|
-
# later un-guarding slice (WD6b) needs
|
|
356
|
-
#
|
|
373
|
+
# later un-guarding slice (WD6b) needs — and the two kinds behave OPPOSITELY on both axes: `:local` is
|
|
374
|
+
# dropped by `with_local` and intersected by `join`, while `:inferred_param` is sticky across `with_local`
|
|
375
|
+
# and unioned by `join`. See {#without_inferred_param_mark} below for the clearing contract, and
|
|
376
|
+
# `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)" for the
|
|
377
|
+
# normative statement of both.
|
|
357
378
|
def with_inferred_param_mark(name)
|
|
358
379
|
rebuild(declaration_sourced: add_declaration_sourced(:inferred_param, name))
|
|
359
380
|
end
|
|
@@ -505,6 +526,43 @@ module Rigor
|
|
|
505
526
|
Inference::DefNodeResolver.resolve(entry)
|
|
506
527
|
end
|
|
507
528
|
|
|
529
|
+
# Issue #316 — the CONFIDENCE-GATED companion of {#top_level_def_for}, and the only accessor the type
|
|
530
|
+
# inference may bind through. {#top_level_def_for} stays unrestricted because it also serves the
|
|
531
|
+
# *suppression* side (`call.unresolved-toplevel`, `call.undefined-method`): a name the project defines at
|
|
532
|
+
# the top level must never be reported as unresolved, whatever this gate decides.
|
|
533
|
+
#
|
|
534
|
+
# Returns nil — decline to bind, stay silent — when BOTH hold:
|
|
535
|
+
#
|
|
536
|
+
# 1. The call site sits inside a block whose `self` is unmodelled ({#opaque_block_self?}) and no narrowed
|
|
537
|
+
# `self_type` says otherwise. A top-level `def` is a private method on `Object`, so it is *callable*
|
|
538
|
+
# from any `self`; what the analyzer cannot see is whether the block's real `self` gained a PUBLIC
|
|
539
|
+
# same-named method by `include` / `extend`, which wins the MRO over the private `Object` def. RSpec's
|
|
540
|
+
# `output` / `include` / `match` matchers against a project's own `def output` are exactly this.
|
|
541
|
+
# 2. The `def` lives in a DIFFERENT file from the call site. Collocation is the evidence that the two
|
|
542
|
+
# belong to one lexical structure — the `RSpec.describe do; def helper; end; it { helper } end` case
|
|
543
|
+
# v0.0.3 A and #319 deliberately serve. Cross-file, the two share only a name.
|
|
544
|
+
#
|
|
545
|
+
# Both conditions are required, so a top-level helper called from genuine top-level code keeps resolving
|
|
546
|
+
# (cross-file included), and a helper defined beside its DSL-block call site keeps resolving too. When the
|
|
547
|
+
# project pre-pass recorded no source for the name, the file test cannot be answered and the historical
|
|
548
|
+
# bind is kept.
|
|
549
|
+
def bindable_top_level_def_for(method_name)
|
|
550
|
+
node = top_level_def_for(method_name)
|
|
551
|
+
return node if node.nil?
|
|
552
|
+
return node unless @opaque_block_self && @self_type.nil?
|
|
553
|
+
|
|
554
|
+
same_file_top_level_def?(method_name) ? node : nil
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def same_file_top_level_def?(method_name)
|
|
558
|
+
key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
559
|
+
site = discovered_def_sources.dig(key, method_name.to_sym)
|
|
560
|
+
return true if site.nil? || @source_path.nil?
|
|
561
|
+
|
|
562
|
+
File.expand_path(site.sub(/:\d+\z/, "")) == File.expand_path(@source_path)
|
|
563
|
+
end
|
|
564
|
+
private :same_file_top_level_def?
|
|
565
|
+
|
|
508
566
|
# ADR-46 slice 3 — a top-level (`def helper` outside any class) call has NO class ancestry to walk, so unlike
|
|
509
567
|
# {#user_def_for} a miss here records no positive ancestry edge that would re-check the consumer when the
|
|
510
568
|
# method later appears. Record the cross-file edge explicitly: the file defining the top-level method
|
|
@@ -756,6 +814,7 @@ module Rigor
|
|
|
756
814
|
declaration_sourced: @declaration_sourced,
|
|
757
815
|
source_path: @source_path,
|
|
758
816
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
817
|
+
opaque_block_self: @opaque_block_self,
|
|
759
818
|
dynamic_origins: @dynamic_origins,
|
|
760
819
|
local_origins: @local_origins,
|
|
761
820
|
ivar_origins: @ivar_origins,
|
|
@@ -774,6 +833,7 @@ module Rigor
|
|
|
774
833
|
declaration_sourced: declaration_sourced,
|
|
775
834
|
source_path: source_path,
|
|
776
835
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
836
|
+
opaque_block_self: opaque_block_self,
|
|
777
837
|
dynamic_origins: dynamic_origins,
|
|
778
838
|
local_origins: local_origins,
|
|
779
839
|
ivar_origins: ivar_origins,
|
|
@@ -14,6 +14,14 @@ module Rigor
|
|
|
14
14
|
#
|
|
15
15
|
# Non-Prism children (literals embedded in node attributes, virtual nodes, or `nil` slots) are silently
|
|
16
16
|
# skipped so callers can rely on every yielded value responding to the `Prism::Node` API.
|
|
17
|
+
#
|
|
18
|
+
# Issue #318 — a `Prism::DefinedNode`'s operand is never evaluated at runtime (`defined?` inspects the
|
|
19
|
+
# expression statically; it does not run it), so the walk yields the `DefinedNode` itself but does NOT
|
|
20
|
+
# descend into its `value` subtree. Every consumer of this walker treats a yielded node as "reachable,
|
|
21
|
+
# evaluated code" (mutation/break/return scans, the check-rules main-pass oracle, coverage and precision
|
|
22
|
+
# probes); walking into the operand would make them reason about code that can never run, which is
|
|
23
|
+
# exactly the false-positive class the issue reports (`defined?(@x) && ...` flagging a call that is
|
|
24
|
+
# actually inert).
|
|
17
25
|
module NodeWalker
|
|
18
26
|
module_function
|
|
19
27
|
|
|
@@ -30,6 +38,8 @@ module Rigor
|
|
|
30
38
|
return unless node.is_a?(Prism::Node)
|
|
31
39
|
|
|
32
40
|
yield node
|
|
41
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
42
|
+
|
|
33
43
|
node.rigor_each_child { |child| walk(child, &) }
|
|
34
44
|
end
|
|
35
45
|
|
|
@@ -52,6 +62,8 @@ module Rigor
|
|
|
52
62
|
return unless node.is_a?(Prism::Node)
|
|
53
63
|
|
|
54
64
|
block.call(node, ancestors)
|
|
65
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
66
|
+
|
|
55
67
|
ancestors.push(node)
|
|
56
68
|
node.rigor_each_child { |child| walk_with_ancestors(child, ancestors, &block) }
|
|
57
69
|
ancestors.pop
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Type
|
|
5
|
+
# #319 — the spelling of a class that Ruby created without a name: the `Class.new do ... end` /
|
|
6
|
+
# `Module.new do ... end` form away from constant-write position, which owns whatever its block body defines
|
|
7
|
+
# but has no constant to be keyed by.
|
|
8
|
+
#
|
|
9
|
+
# `Inference::AnonymousMetaClass` decides WHICH call sites get one and what goes in the key; this module owns
|
|
10
|
+
# the spelling, because the two places a name reaches a human live here. `#<Label:key>` is deliberately
|
|
11
|
+
# unspellable as a Ruby constant path, so a synthetic name can never collide with a real class in the
|
|
12
|
+
# discovery tables.
|
|
13
|
+
#
|
|
14
|
+
# Both renderings drop the key. `describe` keeps only the label (`#<Class>`) because the key is a file
|
|
15
|
+
# position — reproducing it would pin every `assert_type` fixture and precision snapshot to a line number.
|
|
16
|
+
# `erase_to_rbs` answers `untyped`: the name is not valid RBS, and emitting it from `rigor sig-gen` would
|
|
17
|
+
# produce a signature file that does not parse.
|
|
18
|
+
module AnonymousClassName
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
PREFIX = "#<"
|
|
22
|
+
|
|
23
|
+
# `build("Class", "lib/a.rb:3:17") #=> "#<Class:lib/a.rb:3:17>"`
|
|
24
|
+
def build(label, key)
|
|
25
|
+
"#{PREFIX}#{label}:#{key}>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def match?(class_name)
|
|
29
|
+
class_name.is_a?(String) && class_name.start_with?(PREFIX)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The display form: the label alone, key dropped.
|
|
33
|
+
def display(class_name)
|
|
34
|
+
return class_name unless match?(class_name)
|
|
35
|
+
|
|
36
|
+
"#{PREFIX}#{class_name.delete_prefix(PREFIX).split(':', 2).first}>"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|