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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/docs/handbook/08-understanding-errors.md +3 -2
  4. data/docs/install.md +87 -15
  5. data/docs/manual/02-cli-reference.md +2 -2
  6. data/docs/manual/03-configuration.md +5 -0
  7. data/docs/manual/08-skills.md +4 -3
  8. data/docs/manual/12-caching.md +43 -4
  9. data/docs/manual/14-rails-quickstart.md +19 -11
  10. data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
  11. data/lib/rigor/analysis/check_rules.rb +118 -87
  12. data/lib/rigor/analysis/dependency_recorder.rb +200 -18
  13. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  14. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  15. data/lib/rigor/analysis/incremental_session.rb +273 -49
  16. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  17. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  18. data/lib/rigor/analysis/run_cache_key.rb +27 -8
  19. data/lib/rigor/analysis/run_stats.rb +2 -2
  20. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
  21. data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
  22. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  23. data/lib/rigor/analysis/runner.rb +337 -46
  24. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  25. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  26. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  27. data/lib/rigor/analysis/template_units.rb +15 -5
  28. data/lib/rigor/analysis/worker_session.rb +4 -3
  29. data/lib/rigor/cache/descriptor.rb +40 -1
  30. data/lib/rigor/cache/file_digest.rb +28 -0
  31. data/lib/rigor/cache/incremental_snapshot.rb +70 -8
  32. data/lib/rigor/cache/store.rb +63 -3
  33. data/lib/rigor/cache/value_digest.rb +325 -0
  34. data/lib/rigor/cli/check_command.rb +83 -13
  35. data/lib/rigor/cli/doctor_command.rb +1 -1
  36. data/lib/rigor/cli/effects_report.rb +2 -2
  37. data/lib/rigor/cli/unused_command.rb +3 -3
  38. data/lib/rigor/cli.rb +1 -1
  39. data/lib/rigor/config_audit.rb +3 -2
  40. data/lib/rigor/config_path.rb +29 -0
  41. data/lib/rigor/configuration/severity_profile.rb +5 -0
  42. data/lib/rigor/configuration.rb +6 -5
  43. data/lib/rigor/effects/plugin_facts.rb +6 -6
  44. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  45. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  46. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  47. data/lib/rigor/environment/rbs_loader.rb +1 -1
  48. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  49. data/lib/rigor/environment.rb +13 -70
  50. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  51. data/lib/rigor/inference/captured_locals.rb +7 -1
  52. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  53. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  54. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  55. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  56. data/lib/rigor/inference/declaration_walk.rb +7 -0
  57. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  58. data/lib/rigor/inference/definer_resolution.rb +266 -0
  59. data/lib/rigor/inference/expression_typer.rb +136 -45
  60. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  61. data/lib/rigor/inference/guard_rebinding.rb +53 -12
  62. data/lib/rigor/inference/macro_block_self_type.rb +14 -2
  63. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
  64. data/lib/rigor/inference/module_function_state.rb +213 -0
  65. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  66. data/lib/rigor/inference/narrowing.rb +147 -17
  67. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  68. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  69. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  70. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  71. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  72. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  73. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  74. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  75. data/lib/rigor/inference/scope_indexer.rb +1443 -295
  76. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  77. data/lib/rigor/inference/statement_evaluator.rb +80 -14
  78. data/lib/rigor/plugin/base.rb +28 -8
  79. data/lib/rigor/plugin/io_boundary.rb +82 -4
  80. data/lib/rigor/plugin/isolation.rb +1 -1
  81. data/lib/rigor/plugin/trust_policy.rb +3 -3
  82. data/lib/rigor/protection/discovery_seed.rb +18 -9
  83. data/lib/rigor/protection/mutation_cache.rb +3 -3
  84. data/lib/rigor/reflection/constant_ancestors.rb +96 -47
  85. data/lib/rigor/reflection/constant_path.rb +22 -7
  86. data/lib/rigor/reflection.rb +2 -3
  87. data/lib/rigor/scope/discovery_index.rb +218 -2
  88. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  89. data/lib/rigor/scope/resolution_chain.rb +884 -0
  90. data/lib/rigor/scope.rb +295 -322
  91. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  92. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  93. data/lib/rigor/sig_gen/generator.rb +22 -16
  94. data/lib/rigor/type/combinator.rb +25 -1
  95. data/lib/rigor/version.rb +1 -1
  96. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  97. data/sig/rigor/environment.rbs +2 -0
  98. data/sig/rigor/plugin/io_boundary.rbs +1 -0
  99. data/sig/rigor/scope.rbs +43 -1
  100. data/skills/rigor-project-init/SKILL.md +37 -11
  101. data/skills/rigor-project-init/references/02-configure.md +7 -2
  102. 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