rigortype 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +5 -0
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
- data/lib/rigor/analysis/check_rules.rb +118 -87
- data/lib/rigor/analysis/dependency_recorder.rb +200 -18
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +273 -49
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -8
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner.rb +337 -46
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +4 -3
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +70 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +5 -0
- data/lib/rigor/configuration.rb +6 -5
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +1 -1
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +13 -70
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +136 -45
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/guard_rebinding.rb +53 -12
- data/lib/rigor/inference/macro_block_self_type.rb +14 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +147 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +1443 -295
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +80 -14
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +3 -3
- data/lib/rigor/reflection/constant_ancestors.rb +96 -47
- data/lib/rigor/reflection/constant_path.rb +22 -7
- data/lib/rigor/reflection.rb +2 -3
- data/lib/rigor/scope/discovery_index.rb +218 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +295 -322
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +22 -16
- data/lib/rigor/type/combinator.rb +25 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -0
- data/sig/rigor/scope.rbs +43 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +25 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
module ScopeIndexer
|
|
6
|
+
# The accumulator the mixin walks ({ScopeIndexer#mixin_tables}, {ScopeIndexer#extend_tables}) fill: the
|
|
7
|
+
# per-class module lists as before, plus which mixin edges have no position the tables can vouch for
|
|
8
|
+
# (`Scope::DiscoveryIndex#unpositioned_mixins`).
|
|
9
|
+
#
|
|
10
|
+
# The mixin tables store each class's modules in the order the statements that wrote them run, and that
|
|
11
|
+
# order is a fact only for a DIRECT statement of the class's own body (or of its `class << self` body),
|
|
12
|
+
# in a declaration that is itself a direct statement (a top-level statement, or a direct statement of a
|
|
13
|
+
# direct body): `include M` there runs where it is written, unconditionally. An `include` inside a
|
|
14
|
+
# method, a block (`class_eval`, `Class.new`, `included do`), a conditional, or a declaration wrapped in
|
|
15
|
+
# a conditional or a block (`class C; include B; end if X`) runs when that code runs, if it runs at
|
|
16
|
+
# all, and the receiver form (`Base.prepend(M)`) runs wherever it is called from, so the order the
|
|
17
|
+
# tables give such an edge is a guess. The walks record those edges as they always did and name them
|
|
18
|
+
# here, so a reader that depends on ORDER can decline where the order is not known. An edge written
|
|
19
|
+
# both ways is unpositioned: one guessed copy is enough to move the answer.
|
|
20
|
+
#
|
|
21
|
+
# A mixin call the walks CANNOT record (`include helper`, `include(*MODS)`, `send(:include, M)`,
|
|
22
|
+
# `self.include M`, `C.include(M)`, `singleton_class.include M`, `base.extend M` or
|
|
23
|
+
# `base.class_eval { include M }` in a `self.included(base)` hook, a name in an argument list beside one
|
|
24
|
+
# the walk cannot name) still reshapes the ancestry of the class it belongs to, and
|
|
25
|
+
# leaves the edges the tables did record looking positioned. So it taints the whole owner side with the
|
|
26
|
+
# {WILDCARD} name instead: an owner side that lists {WILDCARD} has no known order at all.
|
|
27
|
+
#
|
|
28
|
+
# The value is `{owner => {include: [names], extend: [names]}}`. `:include` is the instance side
|
|
29
|
+
# (`include` and `prepend`), `:extend` the singleton side (`extend`, and `include` / `prepend` inside
|
|
30
|
+
# `class << self`), so an unknown `extend` never taints a class's instance ancestry.
|
|
31
|
+
#
|
|
32
|
+
# A Hash (owner => lists), so every walk helper that indexes the accumulator keeps working unchanged.
|
|
33
|
+
class MixinAccumulator < Hash
|
|
34
|
+
# The name an owner side lists when a mixin call the walk could not record was written on it.
|
|
35
|
+
WILDCARD = "*"
|
|
36
|
+
|
|
37
|
+
# What {#unpositioned} answers when nothing was noted: one shared frozen table, so the common file
|
|
38
|
+
# allocates none.
|
|
39
|
+
EMPTY = {}.freeze
|
|
40
|
+
|
|
41
|
+
DIRECT_STATEMENTS = [Prism::CallNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
42
|
+
private_constant :DIRECT_STATEMENTS
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
super
|
|
46
|
+
@direct = nil
|
|
47
|
+
@unpositioned = nil
|
|
48
|
+
@hook_params = nil
|
|
49
|
+
@body_blocks = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Marks the statements of a body whose own position is a fact — the calls and declarations in them
|
|
53
|
+
# are the only ones whose position is. The caller marks a declaration's body only when the
|
|
54
|
+
# declaration was itself marked (or is the file's top level).
|
|
55
|
+
def direct_body(body)
|
|
56
|
+
return unless body.is_a?(Prism::StatementsNode)
|
|
57
|
+
|
|
58
|
+
body.body.each do |statement|
|
|
59
|
+
next unless DIRECT_STATEMENTS.any? { |kind| statement.is_a?(kind) }
|
|
60
|
+
|
|
61
|
+
(@direct ||= Set.new.compare_by_identity) << statement
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Marks the block-carrying calls that are statements of a class or module body, whatever that declaration
|
|
66
|
+
# sits in: the only blocks whose `self` the extends walk takes to be the body's own (#1592).
|
|
67
|
+
def body_statements(body)
|
|
68
|
+
return unless body.is_a?(Prism::StatementsNode)
|
|
69
|
+
|
|
70
|
+
body.body.each do |statement|
|
|
71
|
+
if statement.is_a?(Prism::CallNode) && statement.block
|
|
72
|
+
(@body_blocks ||= Set.new.compare_by_identity) << statement
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def body_block?(node)
|
|
78
|
+
!@body_blocks.nil? && @body_blocks.include?(node)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Runs the block with the parameters of a `self.included(base)`-style hook in scope: a mixin call on
|
|
82
|
+
# one of them reshapes the class that reached the hook, which this walk cannot name.
|
|
83
|
+
def with_hook_params(names)
|
|
84
|
+
saved = @hook_params
|
|
85
|
+
@hook_params = saved ? saved + names : names
|
|
86
|
+
yield
|
|
87
|
+
ensure
|
|
88
|
+
@hook_params = saved
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def hook_local?(name)
|
|
92
|
+
!@hook_params.nil? && @hook_params.include?(name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def direct?(node)
|
|
96
|
+
!@direct.nil? && @direct.include?(node)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Notes the edges `node` recorded for `owner` on `side`. `complete` is false when an argument named
|
|
100
|
+
# nothing the walk could record, which leaves the recorded names' neighbours unknown.
|
|
101
|
+
def note(node, owner, side, targets, complete:)
|
|
102
|
+
return taint(owner, side) unless complete
|
|
103
|
+
return if node.receiver.nil? && direct?(node)
|
|
104
|
+
|
|
105
|
+
add(owner, side, targets)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Names `owner`'s `side` as having no known order.
|
|
109
|
+
def taint(owner, side)
|
|
110
|
+
add(owner, side, [WILDCARD])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `{owner => {side => [names]}}`, frozen, each list de-duplicated.
|
|
114
|
+
def unpositioned
|
|
115
|
+
return EMPTY if @unpositioned.nil?
|
|
116
|
+
|
|
117
|
+
@unpositioned.transform_values do |sides|
|
|
118
|
+
sides.transform_values { |names| names.uniq.freeze }.freeze
|
|
119
|
+
end.freeze
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def add(owner, side, names)
|
|
125
|
+
((@unpositioned ||= {})[owner] ||= {})[side] = ((@unpositioned[owner][side] || []) + names)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../declaration_walk/traversal"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module ScopeIndexer
|
|
8
|
+
# The as-written superclass table and its issue #682 header-nesting twin (ADR-24 slice 2), as a
|
|
9
|
+
# {DeclarationWalk} collector: each `class Foo < Bar` header's superclass name and the `Module.nesting`
|
|
10
|
+
# its header sits in, plus the superclass of a `Class.new(Parent) { … }` block under that anonymous
|
|
11
|
+
# class's synthetic name (#319). One walk builds both tables, so a caller never pairs a superclass
|
|
12
|
+
# with a nesting from a different parse.
|
|
13
|
+
class SuperclassesCollector
|
|
14
|
+
include DeclarationWalk::Collector
|
|
15
|
+
|
|
16
|
+
# The two rules `walk_class_superclasses` answered differently from the walk, kept so both tables stay
|
|
17
|
+
# byte-identical (ADR-116 WD5). Each is a legacy answer, and #1521 tracks converging it:
|
|
18
|
+
#
|
|
19
|
+
# - `factory_block: :ordinary_call` — the legacy walker had no bare-factory arm, so it walked
|
|
20
|
+
# `Class.new { … }` as any call, block parameters included, with the enclosing `self`. A
|
|
21
|
+
# `class self::E < S` in that block is filed under the enclosing class (`C::E`), a class Ruby never
|
|
22
|
+
# creates (#1521 item 8).
|
|
23
|
+
# - `anonymous_class_path: :outside_class_bodies` — the legacy walker dropped the file path when it
|
|
24
|
+
# entered a class/module, meta-new or eval-family body, but not a `class <<` body, so an anonymous
|
|
25
|
+
# class created inside a class body is keyed without the path the other tables give it (#1521 item
|
|
26
|
+
# 11).
|
|
27
|
+
VARIANTS = { factory_block: :ordinary_call, anonymous_class_path: :outside_class_bodies }.freeze
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@accumulator = { superclasses: {}, header_nestings: {} }
|
|
31
|
+
# Read through the class, as the walk reads `factory_block`, so a subclass that overrides `VARIANTS`
|
|
32
|
+
# gets one answer from both.
|
|
33
|
+
@path_variant = DeclarationWalk::Collector.variant_of(self.class, :anonymous_class_path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A header that still names its class records its ancestry: an unnameable one (`class D` below
|
|
37
|
+
# `class <<`) would publish facts for a `C::D` Ruby never creates. The nesting recorded is the one
|
|
38
|
+
# OUTSIDE the header, because the header is evaluated before its body is entered.
|
|
39
|
+
def on_declaration(node, context, body)
|
|
40
|
+
unless body.singleton_cref
|
|
41
|
+
ScopeIndexer.record_declaration_ancestry(node, context.nesting, body.prefix, @accumulator)
|
|
42
|
+
end
|
|
43
|
+
DeclarationWalk::DESCEND
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Only a `Class.new(Parent) { … }` with a literal block records anything, so a call without one is
|
|
47
|
+
# passed over before the path is looked up.
|
|
48
|
+
def on_call(node, context)
|
|
49
|
+
if node.block.is_a?(Prism::BlockNode)
|
|
50
|
+
path = context.anonymous_class_path(@path_variant)
|
|
51
|
+
ScopeIndexer.record_anonymous_meta_superclass(node, @accumulator[:superclasses], path)
|
|
52
|
+
end
|
|
53
|
+
DeclarationWalk::DESCEND
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# `[superclasses, header_nestings]`, each frozen, as {ScopeIndexer.declaration_walk_tables} returns
|
|
57
|
+
# them.
|
|
58
|
+
def tables
|
|
59
|
+
[@accumulator[:superclasses].freeze, @accumulator[:header_nestings].freeze]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|