rigortype 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../version"
6
+ require_relative "../cache/engine_source"
7
+ require_relative "../analysis/run_cache_key"
8
+
9
+ module Rigor
10
+ module Lens
11
+ # Assembles the `lens/v0` document (docs/internal-spec/lens.md § Document) from a {Run::Outcome}.
12
+ class Document
13
+ SITE_FIELDS = %w[key kind name owner scope attribute members ordinal span contract_span anchor visibility
14
+ contract params block body_return].freeze
15
+
16
+ def initialize(outcome:, configuration:, root:, config_path:)
17
+ @outcome = outcome
18
+ @configuration = configuration
19
+ @root = root
20
+ @config_path = config_path
21
+ end
22
+
23
+ def to_h
24
+ { "schema" => SCHEMA, "revision" => REVISION, "producer" => producer, "context" => context,
25
+ "capabilities" => CAPABILITIES, "files" => @outcome.targets.map { |target| file(target) },
26
+ "members" => @outcome.member_rows }
27
+ end
28
+
29
+ private
30
+
31
+ def producer
32
+ identity = begin
33
+ Cache::EngineSource.process_identity
34
+ rescue StandardError
35
+ "unknown"
36
+ end
37
+ { "name" => "rigor", "version" => Rigor::VERSION, "engine_source" => identity }
38
+ end
39
+
40
+ def context
41
+ fields = {
42
+ "config" => @config_path && relative(File.expand_path(@config_path)),
43
+ "target_ruby" => @configuration.target_ruby,
44
+ "rbs_version" => rbs_version,
45
+ "signature_roots" => signature_roots.map { |root| relative(root) },
46
+ "signature_digest" => signature_digest,
47
+ "plugins" => @outcome.plugins,
48
+ "lockfiles" => lockfiles
49
+ }
50
+ fields.merge("digest" => Lens.digest(JSON.generate(fields)))
51
+ end
52
+
53
+ def rbs_version
54
+ require "rbs"
55
+ RBS::VERSION
56
+ end
57
+
58
+ def signature_roots
59
+ Environment.project_signature_roots(@configuration.signature_paths).map { |path| File.expand_path(path.to_s) }
60
+ end
61
+
62
+ def signature_digest
63
+ listing = signature_roots.flat_map { |root| Dir.glob(File.join(root, "**", "*.rbs")) }.sort.map do |path|
64
+ "#{relative(path)}\t#{Lens.digest(File.binread(path))}\n"
65
+ end
66
+ Lens.digest(listing.join)
67
+ end
68
+
69
+ def lockfiles
70
+ Analysis::RunCacheKey.resolved_lockfile_paths(@configuration).compact.filter_map do |path|
71
+ absolute = File.expand_path(path.to_s)
72
+ next unless File.file?(absolute)
73
+
74
+ { "path" => relative(absolute), "digest" => Lens.digest(File.binread(absolute)) }
75
+ end
76
+ end
77
+
78
+ # Every field is present; those that apply only to some statuses are null elsewhere (lens.md § Files).
79
+ def file(target)
80
+ read = %w[ok parse_error].include?(target.status)
81
+ head = { "argument" => target.argument, "path" => target.status == "not_found" ? nil : target.path,
82
+ "status" => target.status, "reason" => target.status == "ok" ? nil : target.reason,
83
+ "digest" => file_digest(target, read) }
84
+ head.merge(analysed_fields(target.status == "ok" ? target : nil))
85
+ .merge("diagnostics" => read ? target.diagnostics.map { |diagnostic| diagnostic_row(diagnostic) } : nil)
86
+ .merge("inputs" => target.status == "ok" ? target.inputs : nil)
87
+ end
88
+
89
+ # The digest of the bytes the analysis read; a file that did not parse is read once more.
90
+ def file_digest(target, read)
91
+ return nil unless read
92
+ return target.reader.digest if target.reader
93
+
94
+ Lens.digest(File.binread(target.absolute))
95
+ end
96
+
97
+ def analysed_fields(target)
98
+ reader = target&.reader
99
+ { "complete" => reader && reader.omissions.empty?, "omissions" => reader&.omissions,
100
+ "sites" => reader&.sites&.map { |site| site.data.slice(*SITE_FIELDS) } }
101
+ end
102
+
103
+ def diagnostic_row(diagnostic)
104
+ { "rule" => diagnostic.qualified_rule, "severity" => diagnostic.severity.to_s,
105
+ "message" => diagnostic.message, "line" => diagnostic.line, "column" => diagnostic.column }
106
+ end
107
+
108
+ def relative(absolute)
109
+ Pathname(absolute).relative_path_from(Pathname(@root)).to_s
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,433 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "anchor"
6
+ require_relative "slot"
7
+ require_relative "parameters"
8
+ require_relative "../inference/def_return_typer"
9
+ require_relative "../inference/origin_lookup"
10
+ require_relative "../sig_gen/type_elaborator"
11
+
12
+ module Rigor
13
+ module Lens
14
+ # Reads one analysed file into sites and omissions (docs/internal-spec/lens.md § Files, § Sites). Runs inside
15
+ # the check run, on the seeded scope index the check rules read, so a body return is the type `check` gives
16
+ # that body. Every declaration the walk meets is either a site or an omission; none is dropped.
17
+ class FileReader # rubocop:disable Metrics/ClassLength
18
+ # A site plus what the member pass needs from it: the declaration node and the contract comments above it.
19
+ Site = Data.define(:data, :node, :contract_comments)
20
+ Result = Data.define(:sites, :omissions, :digest)
21
+ # The running visibility of one class body: Ruby's default for its later `def`s, and whether a toggle the
22
+ # walk cannot place (under an `if`, a `begin`, a block) leaves that default unknown.
23
+ # A `detached` body stands for code that is not the class body (a block, a method body, a superclass
24
+ # expression, an omitted class): its toggles and named calls stay local and change no site.
25
+ Body = Struct.new(:current, :uncertain, :detached)
26
+
27
+ ATTRIBUTE_CALLS = { attr_reader: "reader", attr_writer: "writer", attr_accessor: "accessor" }.freeze
28
+ OMITTED_CALLS = {
29
+ alias_method: "alias", define_method: "define_method", module_function: "module_function",
30
+ refine: "refine"
31
+ }.freeze
32
+ VISIBILITY_CALLS = %i[private protected public].freeze
33
+ CLASS_VISIBILITY_CALLS = { private_class_method: "private", public_class_method: "public" }.freeze
34
+ BLOCK_NODES = [Prism::BlockNode, Prism::LambdaNode].freeze
35
+ CONSTANT_WRITES = [
36
+ Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
37
+ Prism::ConstantPathOrWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantPathAndWriteNode,
38
+ Prism::ConstantOperatorWriteNode, Prism::ConstantPathOperatorWriteNode
39
+ ].freeze
40
+
41
+ # @param path — the file, relative to the project root
42
+ # @param spell — callable `(type, owner) -> String` spelling a type as sig-gen writes it
43
+ def initialize(path:, parse_result:, index:, spell:)
44
+ @path = path
45
+ @root = parse_result.value
46
+ @source = parse_result.source.source
47
+ @index = index
48
+ @spell = spell
49
+ @anchor = Anchor.new(@source, parse_result.comments)
50
+ @bodies = []
51
+ @node_visibility = {}.compare_by_identity
52
+ @sites = []
53
+ @omissions = []
54
+ end
55
+
56
+ def read
57
+ walk(@root, [], singleton: false, in_block: false)
58
+ Result.new(sites: @sites, omissions: @omissions, digest: Lens.digest(@source.b))
59
+ end
60
+
61
+ private
62
+
63
+ def walk(node, nesting, singleton:, in_block:)
64
+ case node
65
+ when Prism::ClassNode, Prism::ModuleNode then walk_namespace(node, nesting, in_block)
66
+ when Prism::SingletonClassNode then walk_singleton_class(node, nesting, singleton, in_block)
67
+ when Prism::DefNode then read_def(node, nesting, singleton, in_block)
68
+ when Prism::CallNode then walk_call(node, nesting, singleton, in_block)
69
+ when Prism::AliasMethodNode
70
+ omit("alias", node.new_name.slice.delete_prefix(":"), node, owner: nesting.last, singleton: singleton)
71
+ when Prism::MultiWriteNode then walk_multi_write(node, nesting, singleton, in_block)
72
+ when *CONSTANT_WRITES then walk_constant(node, nesting)
73
+ else walk_children(node, nesting, singleton, in_block)
74
+ end
75
+ end
76
+
77
+ def walk_children(node, nesting, singleton, in_block)
78
+ node.compact_child_nodes.each do |child|
79
+ if BLOCK_NODES.include?(child.class)
80
+ detached { walk(child, nesting, singleton: singleton, in_block: true) }
81
+ else
82
+ walk(child, nesting, singleton: singleton, in_block: in_block)
83
+ end
84
+ end
85
+ end
86
+
87
+ def detached
88
+ @bodies.push(Body.new("public", false, true))
89
+ yield
90
+ ensure
91
+ @bodies.pop
92
+ end
93
+
94
+ def walk_namespace(node, nesting, in_block)
95
+ kind = node.is_a?(Prism::ClassNode) ? "class" : "module"
96
+ name = qualify(nesting, node.constant_path)
97
+ if node.is_a?(Prism::ClassNode) && node.superclass
98
+ detached { walk(node.superclass, nesting, singleton: false, in_block: true) }
99
+ end
100
+ if in_block || name.nil?
101
+ omit(kind, name || node.constant_path.slice, node)
102
+ detached { walk(node.body, nesting, singleton: false, in_block: true) } if node.body
103
+ return
104
+ end
105
+
106
+ add_site(node, { "kind" => kind, "name" => name, "owner" => nil, "scope" => nil, "attribute" => nil,
107
+ "members" => [], "visibility" => nil, "params" => nil, "block" => nil,
108
+ "body_return" => nil })
109
+ walk_body(node.body, nesting + [name], singleton: false)
110
+ end
111
+
112
+ # `class << self` directly in a class or module body opens its singleton class. Any other `class << x`, one
113
+ # nested in another, or one at the top level (which opens `main`'s) is not modelled.
114
+ def walk_singleton_class(node, nesting, singleton, in_block)
115
+ if in_block || singleton || nesting.empty? || !node.expression.is_a?(Prism::SelfNode)
116
+ omit("singleton_class", nil, node)
117
+ return detached { walk(node.body, nesting, singleton: singleton, in_block: true) } if node.body
118
+ end
119
+
120
+ walk_body(node.body, nesting, singleton: true)
121
+ end
122
+
123
+ # A class body's statements, tracking the running visibility and `module_function`. A bare `module_function`
124
+ # makes every later `def` of the body a method on both sides: one site, two members, which revision 1 does
125
+ # not model.
126
+ def walk_body(body, nesting, singleton:)
127
+ return if body.nil?
128
+
129
+ @bodies.push(Body.new("public", false, false))
130
+ statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
131
+ module_function_on = false
132
+ statements.each do |child|
133
+ if module_function_on && child.is_a?(Prism::DefNode)
134
+ omit("module_function", child.name.to_s, child, owner: nesting.last, singleton: singleton)
135
+ next
136
+ end
137
+ module_function_on = true if bare_call?(child, :module_function)
138
+ next if toggle_visibility?(child)
139
+
140
+ visibility_call(child, nesting, singleton, direct: true) if visibility_call?(child)
141
+ walk(child, nesting, singleton: singleton, in_block: false)
142
+ end
143
+ ensure
144
+ @bodies.pop if body
145
+ end
146
+
147
+ # A bare `private` / `protected` / `public` written directly in the body sets the default for later defs.
148
+ def toggle_visibility?(child)
149
+ return false unless VISIBILITY_CALLS.any? { |name| bare_call?(child, name) }
150
+
151
+ @bodies.last.current = child.name.to_s
152
+ @bodies.last.uncertain = false
153
+ true
154
+ end
155
+
156
+ def walk_call(node, nesting, singleton, in_block)
157
+ if node.receiver.nil? && ATTRIBUTE_CALLS.key?(node.name)
158
+ read_attribute(node, nesting, singleton, in_block)
159
+ elsif node.receiver.nil? && OMITTED_CALLS.key?(node.name)
160
+ return if bare_call?(node, :module_function)
161
+
162
+ omit(OMITTED_CALLS.fetch(node.name), first_argument_name(node), node,
163
+ owner: node.name == :refine ? nil : nesting.last, singleton: singleton)
164
+ walk_children(node, nesting, singleton, true) unless node.name == :refine
165
+ else
166
+ @visited_visibility_calls ||= {}.compare_by_identity
167
+ if visibility_call?(node) && !@visited_visibility_calls.key?(node)
168
+ visibility_call(node, nesting, singleton, direct: false)
169
+ end
170
+ walk_children(node, nesting, singleton, in_block)
171
+ end
172
+ end
173
+
174
+ def visibility_call?(node)
175
+ node.is_a?(Prism::CallNode) && node.receiver.nil? &&
176
+ (VISIBILITY_CALLS.include?(node.name) || CLASS_VISIBILITY_CALLS.key?(node.name))
177
+ end
178
+
179
+ # `private :x`, `private %i[x]`, `private def x`, `private attr_reader :x`, `private_class_method :x`, and a
180
+ # bare toggle the walk cannot place in the body's statement list. A named call changes members only when it
181
+ # is written directly in the body (`direct`); one under an `if`, a block or a method body runs at a time
182
+ # Rigor does not know, so the members it names read unknown.
183
+ def visibility_call(node, nesting, singleton, direct:)
184
+ @visited_visibility_calls ||= {}.compare_by_identity
185
+ @visited_visibility_calls[node] = true
186
+ direct &&= !@bodies.last&.detached
187
+ if VISIBILITY_CALLS.include?(node.name)
188
+ return mark_uncertain if node.arguments.nil?
189
+
190
+ apply_visibility(node, node.name.to_s, nesting.last, singleton, direct)
191
+ else
192
+ apply_visibility(node, CLASS_VISIBILITY_CALLS.fetch(node.name), nesting.last, true, direct)
193
+ end
194
+ end
195
+
196
+ def mark_uncertain
197
+ @bodies.last&.uncertain = true
198
+ end
199
+
200
+ def apply_visibility(node, visibility, owner, singleton, direct)
201
+ scope = singleton ? "singleton" : "instance"
202
+ node.arguments.arguments.each do |argument|
203
+ names = argument_names(argument)
204
+ if wrapped_declaration?(argument, singleton)
205
+ @node_visibility[argument] = visibility
206
+ elsif argument.is_a?(Prism::DefNode)
207
+ # `private def self.x`: Ruby looks `x` up among instance methods and rejects it, so the result is unknown.
208
+ @node_visibility[argument] = nil
209
+ elsif names.nil?
210
+ # `private *NAMES`: which members change is unknown.
211
+ unknown_visibility(owner, scope)
212
+ else
213
+ names.each { |name| name_visibility(owner, scope, name, direct ? visibility : nil) }
214
+ end
215
+ end
216
+ end
217
+
218
+ # A `def` or `attr_*` the visibility call wraps. `private def self.x` is not one: `private` looks the
219
+ # name up among instance methods.
220
+ def wrapped_declaration?(argument, singleton)
221
+ case argument
222
+ when Prism::DefNode then argument.receiver.nil? || singleton
223
+ when Prism::CallNode then argument.receiver.nil? && ATTRIBUTE_CALLS.key?(argument.name)
224
+ else false
225
+ end
226
+ end
227
+
228
+ # The names a visibility call's argument lists, or nil when it is not a literal list of them.
229
+ def argument_names(argument)
230
+ case argument
231
+ when Prism::SymbolNode, Prism::StringNode then [argument.unescaped]
232
+ when Prism::ArrayNode
233
+ names = argument.elements.map { |element| argument_names(element)&.first }
234
+ names.include?(nil) ? nil : names
235
+ end
236
+ end
237
+
238
+ def unknown_visibility(owner, scope)
239
+ @sites.each do |site|
240
+ site.data["visibility"] = nil if site.data["owner"] == owner && site.data["scope"] == scope
241
+ end
242
+ mark_uncertain
243
+ end
244
+
245
+ # `private :x` changes the visibility of the members already defined under that name in this file.
246
+ def name_visibility(owner, scope, name, visibility)
247
+ @sites.each do |site|
248
+ data = site.data
249
+ next unless data["owner"] == owner && data["scope"] == scope && data["members"].include?(name)
250
+
251
+ data["visibility"] = data["members"] == [name] ? visibility : nil
252
+ end
253
+ end
254
+
255
+ def walk_multi_write(node, nesting, singleton, in_block)
256
+ (node.lefts + [node.rest&.expression].compact + node.rights).each do |target|
257
+ next unless target.is_a?(Prism::ConstantTargetNode) || target.is_a?(Prism::ConstantPathTargetNode)
258
+
259
+ omit("constant", qualify(nesting, target) || target.slice, target)
260
+ end
261
+ walk(node.value, nesting, singleton: singleton, in_block: in_block)
262
+ end
263
+
264
+ def walk_constant(node, nesting)
265
+ target = node.respond_to?(:target) ? node.target : node
266
+ name = node.respond_to?(:target) ? qualify(nesting, node.target) : qualify_name(nesting, node.name.to_s)
267
+ kind = case data_or_struct(node.value)
268
+ when :data then "data_class"
269
+ when :struct then "struct_class"
270
+ else "constant"
271
+ end
272
+ omit(kind, name || target.slice, node)
273
+ walk(node.value, nesting, singleton: false, in_block: true)
274
+ end
275
+
276
+ def read_def(node, nesting, singleton, in_block)
277
+ scope = def_scope(node, nesting, singleton)
278
+ if in_block || scope.nil?
279
+ omit("method", node.name.to_s, node)
280
+ else
281
+ owner = nesting.last || "Object"
282
+ data = { "kind" => "method", "name" => node.name.to_s, "owner" => owner, "scope" => scope.to_s,
283
+ "attribute" => nil, "members" => [node.name.to_s],
284
+ "visibility" => visibility(node, nesting, scope) }
285
+ params, block, = Parameters.read(node.parameters, yields: Parameters.yields?(node.body))
286
+ shape_params, shape_block = Parameters.shape(params, block)
287
+ data.merge!("params" => shape_params, "block" => shape_block,
288
+ "body_return" => body_return(node, owner, scope))
289
+ add_site(node, data)
290
+ end
291
+ # A declaration inside a method body runs only when the method does: not modelled.
292
+ detached { walk(node.body, nesting, singleton: singleton, in_block: true) } if node.body
293
+ end
294
+
295
+ def read_attribute(node, nesting, singleton, in_block)
296
+ arguments = node.arguments&.arguments || []
297
+ names = arguments.map { |arg| arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode) ? arg.unescaped : nil }
298
+ if in_block || nesting.empty? || names.empty? || names.include?(nil)
299
+ return omit("attribute", names.compact.first, node)
300
+ end
301
+
302
+ owner = nesting.last
303
+ scope = singleton ? :singleton : :instance
304
+ attribute = ATTRIBUTE_CALLS.fetch(node.name)
305
+ names.each_with_index do |name, ordinal|
306
+ add_site(node, { "kind" => "attribute", "name" => name, "owner" => owner, "scope" => scope.to_s,
307
+ "attribute" => attribute, "members" => attribute_members(name, attribute),
308
+ "visibility" => visibility(node, nesting, scope),
309
+ "params" => nil, "block" => nil, "body_return" => nil }, ordinal: ordinal)
310
+ end
311
+ end
312
+
313
+ def add_site(node, data, ordinal: 0)
314
+ location = node.location
315
+ anchor = @anchor.for_span(location.start_offset, location.end_offset, location.start_line,
316
+ location.end_line)
317
+ key = "#{@path}:#{location.start_offset}-#{location.end_offset}:#{ordinal}"
318
+ tail = { "ordinal" => ordinal, "span" => Lens.span(location),
319
+ "contract_span" => Lens.contract_span(anchor.contract_comments), "anchor" => anchor.token,
320
+ "contract" => nil }
321
+ @sites << Site.new(data: { "key" => key }.merge(data).merge(tail), node: node,
322
+ contract_comments: anchor.contract_comments)
323
+ end
324
+
325
+ # `owner` / `singleton` name the member the declaration would define, when Rigor can tell.
326
+ def omit(kind, name, node, owner: nil, singleton: false)
327
+ member = owner && name
328
+ @omissions << { "kind" => kind, "owner" => member ? owner : nil,
329
+ "scope" => member ? scope_name(singleton) : nil, "name" => name,
330
+ "span" => Lens.span(node.location), "reason" => "unsupported_kind" }
331
+ nil
332
+ end
333
+
334
+ def scope_name(singleton)
335
+ singleton ? "singleton" : "instance"
336
+ end
337
+
338
+ def attribute_members(name, attribute)
339
+ case attribute
340
+ when "reader" then [name]
341
+ when "writer" then ["#{name}="]
342
+ else [name, "#{name}="]
343
+ end
344
+ end
345
+
346
+ # The member's visibility as Ruby gives it: a `private def` / `private attr_reader` wrapper, else the running
347
+ # default of the body (unknown after a toggle the walk could not place). A class body's toggles do not reach
348
+ # a `def self.x`. A later `private :x` revises it ({#name_visibility}). A top-level `def` is private.
349
+ def visibility(node, nesting, scope)
350
+ return "private" if nesting.empty?
351
+ return @node_visibility[node] if @node_visibility.key?(node)
352
+ return "public" if scope == :singleton && node.is_a?(Prism::DefNode) && node.receiver
353
+
354
+ body = @bodies.last
355
+ return nil if body.nil? || body.uncertain
356
+
357
+ body.current
358
+ end
359
+
360
+ # `def x` in a class body is an instance method, in `class << self` a singleton one; `def self.x` is a
361
+ # singleton method of a class body. A `def self.x` inside `class << self`, a top-level `def self.x`, and any
362
+ # other receiver define methods revision 1 does not model.
363
+ def def_scope(node, nesting, singleton)
364
+ return (singleton ? :singleton : :instance) if node.receiver.nil?
365
+ return nil unless node.receiver.is_a?(Prism::SelfNode)
366
+ return nil if singleton || nesting.empty?
367
+
368
+ :singleton
369
+ end
370
+
371
+ # The body's return under the seeded index, with its parameters as declared (§ Sites).
372
+ def body_return(node, owner, scope)
373
+ return Slot.typed("void", "inferred") if scope == :instance && node.name == :initialize
374
+ return Slot.typed("nil", "inferred") if node.body.nil?
375
+
376
+ type = Inference::DefReturnTyper.call(node, @index)
377
+ return Slot.unavailable("not_computed") if type.nil?
378
+ return Slot.dynamic("inferred", return_cause(node)) if type.is_a?(Type::Dynamic)
379
+
380
+ spelled = @spell.call(type, owner)
381
+ spelled == "untyped" ? Slot.dynamic("inferred", return_cause(node)) : Slot.typed(spelled, "inferred")
382
+ rescue StandardError
383
+ Slot.unavailable("not_computed")
384
+ end
385
+
386
+ def return_cause(node)
387
+ last = Inference::DefReturnTyper.body_last_expression(node.body)
388
+ scope = last && (@index[last] || @index[node.body])
389
+ scope && Inference::OriginLookup.origin_for(scope, last)
390
+ end
391
+
392
+ # The qualified name a constant path declares under `nesting`, or nil when it is not a plain constant path
393
+ # (`self::Foo`, `expr::Foo`).
394
+ def qualify(nesting, path)
395
+ case path
396
+ when Prism::ConstantReadNode, Prism::ConstantTargetNode then qualify_name(nesting, path.name.to_s)
397
+ when Prism::ConstantPathNode, Prism::ConstantPathTargetNode
398
+ return path.name.to_s if path.parent.nil?
399
+
400
+ parent = qualify(nesting, path.parent)
401
+ parent && "#{parent}::#{path.name}"
402
+ end
403
+ end
404
+
405
+ def qualify_name(nesting, name)
406
+ nesting.empty? ? name : "#{nesting.last}::#{name}"
407
+ end
408
+
409
+ def bare_call?(node, name)
410
+ node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == name && node.arguments.nil? &&
411
+ node.block.nil?
412
+ end
413
+
414
+ def first_argument_name(node)
415
+ argument = node.arguments&.arguments&.first
416
+ case argument
417
+ when Prism::SymbolNode, Prism::StringNode then argument.unescaped
418
+ when Prism::DefNode then argument.name.to_s
419
+ end
420
+ end
421
+
422
+ def data_or_struct(value)
423
+ return nil unless value.is_a?(Prism::CallNode) && value.receiver
424
+
425
+ receiver = value.receiver.slice.delete_prefix("::")
426
+ return :data if receiver == "Data" && value.name == :define
427
+ return :struct if receiver == "Struct" && value.name == :new
428
+
429
+ nil
430
+ end
431
+ end
432
+ end
433
+ end