rigortype 0.3.6 → 0.3.8

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 (223) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
  14. data/docs/handbook/11-sig-gen.md +24 -14
  15. data/docs/manual/02-cli-reference.md +23 -7
  16. data/docs/manual/04-diagnostics.md +1 -1
  17. data/docs/manual/05-inspecting-types.md +20 -8
  18. data/docs/manual/07-plugins.md +4 -0
  19. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  20. data/docs/manual/plugins/rigor-activerecord.md +74 -1
  21. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  22. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  24. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  25. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  27. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  28. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  29. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  30. data/lib/rigor/analysis/check_rules.rb +383 -48
  31. data/lib/rigor/analysis/crash_signature.rb +188 -0
  32. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  33. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  34. data/lib/rigor/analysis/incremental.rb +28 -0
  35. data/lib/rigor/analysis/incremental_session.rb +63 -16
  36. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  37. data/lib/rigor/analysis/reachability/graph.rb +14 -5
  38. data/lib/rigor/analysis/reachability/scan.rb +7 -3
  39. data/lib/rigor/analysis/result.rb +32 -0
  40. data/lib/rigor/analysis/run_cache_key.rb +41 -1
  41. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +233 -16
  42. data/lib/rigor/analysis/runner/pool_coordinator.rb +266 -47
  43. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  44. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -2
  45. data/lib/rigor/analysis/runner.rb +212 -16
  46. data/lib/rigor/analysis/worker_session.rb +37 -3
  47. data/lib/rigor/bleeding_edge.rb +7 -6
  48. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  49. data/lib/rigor/builtins/imported_refinements.rb +11 -9
  50. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  51. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  52. data/lib/rigor/cache/annotation_location.rb +72 -0
  53. data/lib/rigor/cache/descriptor.rb +42 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  55. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +70 -8
  56. data/lib/rigor/cache/store.rb +17 -1
  57. data/lib/rigor/cli/coverage_command.rb +23 -18
  58. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  59. data/lib/rigor/cli/coverage_scan.rb +47 -6
  60. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  61. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  62. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  63. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  64. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  65. data/lib/rigor/cli/sig_gen_command.rb +47 -0
  66. data/lib/rigor/cli/type_of_command.rb +188 -41
  67. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  68. data/lib/rigor/cli/type_scan_command.rb +10 -1
  69. data/lib/rigor/cli.rb +1 -1
  70. data/lib/rigor/configuration.rb +7 -4
  71. data/lib/rigor/effects/plugin_facts.rb +1 -1
  72. data/lib/rigor/effects/scanner.rb +2 -3
  73. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  74. data/lib/rigor/environment/failure_slot.rb +28 -0
  75. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  76. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  77. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  78. data/lib/rigor/environment/rbs_loader.rb +743 -93
  79. data/lib/rigor/environment.rb +125 -25
  80. data/lib/rigor/inference/acceptance.rb +172 -20
  81. data/lib/rigor/inference/captured_locals.rb +62 -0
  82. data/lib/rigor/inference/content_join.rb +347 -0
  83. data/lib/rigor/inference/def_handle.rb +15 -3
  84. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  85. data/lib/rigor/inference/expression_typer.rb +1020 -141
  86. data/lib/rigor/inference/fork_map.rb +6 -1
  87. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  88. data/lib/rigor/inference/hkt_registry.rb +61 -3
  89. data/lib/rigor/inference/hkt_sugar_translator.rb +87 -0
  90. data/lib/rigor/inference/index_write_widening.rb +48 -0
  91. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  92. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  93. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  94. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  95. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  96. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  97. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  98. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  99. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  100. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  101. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  103. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  104. data/lib/rigor/inference/mutation_widening.rb +244 -174
  105. data/lib/rigor/inference/narrowing.rb +331 -27
  106. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  107. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  108. data/lib/rigor/inference/precision_scanner.rb +5 -2
  109. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  110. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  111. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  112. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  113. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  114. data/lib/rigor/inference/version_guard.rb +229 -0
  115. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  116. data/lib/rigor/plugin/base.rb +3 -2
  117. data/lib/rigor/plugin/inflector.rb +14 -5
  118. data/lib/rigor/plugin/io_boundary.rb +107 -4
  119. data/lib/rigor/plugin/loader.rb +14 -0
  120. data/lib/rigor/plugin/registry.rb +25 -2
  121. data/lib/rigor/protection/analysis_guard.rb +60 -0
  122. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  123. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  124. data/lib/rigor/protection/discovery_seed.rb +1 -1
  125. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  126. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  127. data/lib/rigor/protection/mutator.rb +121 -18
  128. data/lib/rigor/rbs_extended/envelope_scanner.rb +8 -7
  129. data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
  130. data/lib/rigor/rbs_extended/reporter.rb +93 -13
  131. data/lib/rigor/rbs_extended.rb +6 -1
  132. data/lib/rigor/reflection.rb +182 -30
  133. data/lib/rigor/scope/discovery_index.rb +39 -1
  134. data/lib/rigor/scope.rb +385 -4
  135. data/lib/rigor/sig_gen/classification.rb +12 -1
  136. data/lib/rigor/sig_gen/generator.rb +320 -85
  137. data/lib/rigor/sig_gen/renderer.rb +14 -12
  138. data/lib/rigor/sig_gen/writer.rb +3 -0
  139. data/lib/rigor/source/constant_path.rb +79 -2
  140. data/lib/rigor/triage/catalogue.rb +1 -1
  141. data/lib/rigor/type/combinator.rb +10 -0
  142. data/lib/rigor/type/maybe.rb +47 -0
  143. data/lib/rigor/type/refined.rb +1 -2
  144. data/lib/rigor/type/result.rb +53 -0
  145. data/lib/rigor/type.rb +2 -0
  146. data/lib/rigor/version.rb +1 -1
  147. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  148. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  149. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  151. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  152. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  157. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  158. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  159. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  160. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  161. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  162. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  163. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  164. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  165. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  166. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  167. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  168. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  169. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  170. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  171. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  172. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  173. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  174. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  175. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  176. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  177. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  178. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  179. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  180. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  181. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  182. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  183. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  184. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  185. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  186. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  187. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  188. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  189. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  190. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  191. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  192. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  193. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  194. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  195. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  196. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  197. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  198. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  199. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  200. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  201. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  202. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  203. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  204. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  205. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  206. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  207. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  208. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  209. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  210. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  211. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  212. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  213. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  214. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  215. data/sig/rigor/analysis/baseline.rbs +1 -1
  216. data/sig/rigor/environment.rbs +9 -0
  217. data/sig/rigor/inference.rbs +4 -1
  218. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  219. data/sig/rigor/reflection.rbs +3 -1
  220. data/sig/rigor/scope.rbs +25 -1
  221. data/sig/rigor/type.rbs +95 -1
  222. data/sig/rigor.rbs +16 -0
  223. metadata +71 -3
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/plugin"
4
+ require "rigor/inference/hkt_registry"
5
+ require "rigor/inference/hkt_body_parser"
6
+
7
+ module Rigor
8
+ module Plugin
9
+ # rigor-dry-monads — Tier A plugin for dry-monads Result and Maybe carriers per
10
+ # [ADR-20](../../../../../docs/adr/20-lightweight-hkt.md) Slice 4.
11
+ class DryMonads < Rigor::Plugin::Base
12
+ RESULT_REGISTRATION = Inference::HktRegistry::Registration.new(
13
+ uri: :"dry_monads::result",
14
+ arity: 2,
15
+ variance: %i[out out],
16
+ bound: Type::Combinator.result_of(Type::Combinator.untyped, Type::Combinator.untyped)
17
+ )
18
+
19
+ MAYBE_REGISTRATION = Inference::HktRegistry::Registration.new(
20
+ uri: :"dry_monads::maybe",
21
+ arity: 1,
22
+ variance: %i[out],
23
+ bound: Type::Combinator.maybe_of(Type::Combinator.untyped)
24
+ )
25
+
26
+ RESULT_DEFINITION = Inference::HktRegistry.definition_with_body_tree(
27
+ uri: :"dry_monads::result",
28
+ params: %i[T E],
29
+ body_tree: Inference::HktBodyParser.parse("Result[T, E]", params: %i[T E]),
30
+ source_path: __FILE__,
31
+ source_line: __LINE__
32
+ )
33
+
34
+ MAYBE_DEFINITION = Inference::HktRegistry.definition_with_body_tree(
35
+ uri: :"dry_monads::maybe",
36
+ params: %i[T],
37
+ body_tree: Inference::HktBodyParser.parse("Maybe[T]", params: %i[T]),
38
+ source_path: __FILE__,
39
+ source_line: __LINE__
40
+ )
41
+
42
+ CONSTRUCTOR_METHODS = %i[Success Failure Some None].freeze
43
+ UNWRAP_METHODS = %i[value! failure value_or].freeze
44
+
45
+ manifest(
46
+ id: "dry-monads",
47
+ version: "0.1.0",
48
+ description: "Lightweight HKT support for dry-monads Result and Maybe carriers",
49
+ hkt_registrations: [RESULT_REGISTRATION, MAYBE_REGISTRATION],
50
+ hkt_definitions: [RESULT_DEFINITION, MAYBE_DEFINITION]
51
+ )
52
+
53
+ dynamic_return methods: -> { CONSTRUCTOR_METHODS } do |call_node, scope|
54
+ args = call_node.arguments&.arguments || []
55
+ case call_node.name
56
+ when :Success
57
+ val = args.first ? scope.type_of(args.first) : Type::Combinator.untyped
58
+ Type::Combinator.result_of(val, Type::Combinator.bot)
59
+ when :Failure
60
+ err = args.first ? scope.type_of(args.first) : Type::Combinator.untyped
61
+ Type::Combinator.result_of(Type::Combinator.bot, err)
62
+ when :Some
63
+ val = args.first ? scope.type_of(args.first) : Type::Combinator.untyped
64
+ Type::Combinator.maybe_of(val)
65
+ when :None
66
+ Type::Combinator.maybe_of(Type::Combinator.bot)
67
+ end
68
+ end
69
+
70
+ dynamic_return receivers: %w[Dry::Monads::Result Dry::Monads::Maybe Result Maybe],
71
+ methods: -> { UNWRAP_METHODS } do |call_node, scope|
72
+ receiver = call_node.receiver
73
+ next nil if receiver.nil?
74
+
75
+ rec_type = scope.type_of(receiver)
76
+ args = call_node.arguments&.arguments || []
77
+ case call_node.name
78
+ when :value!
79
+ if rec_type.is_a?(Type::Result)
80
+ rec_type.ok_type
81
+ elsif rec_type.is_a?(Type::Maybe)
82
+ rec_type.value_type
83
+ end
84
+ when :failure
85
+ rec_type.err_type if rec_type.is_a?(Type::Result)
86
+ when :value_or
87
+ fallback = args.first ? scope.type_of(args.first) : Type::Combinator.untyped
88
+ if rec_type.is_a?(Type::Result)
89
+ Type::Combinator.union(rec_type.ok_type, fallback)
90
+ elsif rec_type.is_a?(Type::Maybe)
91
+ Type::Combinator.union(rec_type.value_type, fallback)
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ Rigor::Plugin.register(DryMonads)
98
+ end
99
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Gem entry point for rigor-dry-monads. Required by Rigor's plugin loader when `.rigor.yml` lists
4
+ # `rigor-dry-monads` under `plugins:`.
5
+ require_relative "rigor/plugin/dry_monads"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/plugin"
4
+ require "rigor-ffi"
5
+
6
+ module Rigor
7
+ module Plugin
8
+ class Ethon < Base
9
+ manifest(
10
+ id: "ethon",
11
+ version: "0.1.0",
12
+ description: "Models Ethon / Libcurl options, dynamic return types, and multi-loop operations.",
13
+ signature_paths: ["sig"]
14
+ )
15
+
16
+ # Option catalogue and return types for Easy methods
17
+ EASY_RETURN_TYPES = {
18
+ perform: -> { Rigor::Type::Combinator.nominal_of("Integer") },
19
+ response_code: -> { Rigor::Type::Combinator.nominal_of("Integer") },
20
+ return_code: -> { Rigor::Type::Combinator.nominal_of("Symbol") },
21
+ total_time: -> { Rigor::Type::Combinator.nominal_of("Float") },
22
+ response_body: -> { Rigor::Type::Combinator.nominal_of("String") },
23
+ response_headers: -> { Rigor::Type::Combinator.nominal_of("String") }
24
+ }.freeze
25
+
26
+ dynamic_return receivers: ["Ethon::Easy"], methods: EASY_RETURN_TYPES.keys do |call_node, _scope|
27
+ builder = EASY_RETURN_TYPES[call_node.name]
28
+ builder&.call
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ Rigor::Plugin.register(Rigor::Plugin::Ethon)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rigor/plugin/ethon"
@@ -0,0 +1,27 @@
1
+ module Ethon
2
+ class Easy
3
+ def initialize: (?Hash[Symbol, untyped] options) -> void
4
+ def url=: (String url) -> String
5
+ def url: () -> String?
6
+ def perform: () -> Integer
7
+ def response_code: () -> Integer
8
+ def return_code: () -> Symbol
9
+ def total_time: () -> Float
10
+ def response_body: () -> String
11
+ def response_headers: () -> String
12
+ def handle: () -> FFI::Pointer
13
+ end
14
+
15
+ class Multi
16
+ def initialize: (?Hash[Symbol, untyped] options) -> void
17
+ def add: (Easy easy) -> void
18
+ def perform: () -> void
19
+ end
20
+
21
+ module Curl
22
+ def self.easy_init: () -> FFI::Pointer
23
+ def self.easy_cleanup: (FFI::Pointer handle) -> void
24
+ def self.easy_perform: (FFI::Pointer handle) -> Integer
25
+ def self.set_option: (Symbol option, untyped value, FFI::Pointer handle) -> Integer
26
+ end
27
+ end
@@ -51,9 +51,10 @@ module Rigor
51
51
  def ruby_files_under(roots)
52
52
  roots.flat_map do |root|
53
53
  absolute = File.expand_path(root)
54
- if File.file?(absolute)
54
+ # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
55
+ if @io_boundary.file?(absolute)
55
56
  [absolute]
56
- elsif File.directory?(absolute)
57
+ elsif @io_boundary.directory?(absolute)
57
58
  Dir.glob(File.join(absolute, "**", "*.rb"))
58
59
  else
59
60
  []
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ class FFI < Base
8
+ module Analyzer
9
+ module_function
10
+
11
+ def extract_symbol(node)
12
+ case node
13
+ when Prism::SymbolNode
14
+ node.unescaped.to_sym
15
+ when Prism::StringNode
16
+ node.unescaped.to_sym
17
+ when Prism::CallNode
18
+ extract_symbol(node.receiver) if node.name == :to_sym && node.receiver
19
+ end
20
+ end
21
+
22
+ def extract_type_symbol(node)
23
+ case node
24
+ when Prism::SymbolNode
25
+ node.unescaped.to_sym
26
+ when Prism::StringNode
27
+ node.unescaped.to_sym
28
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
29
+ node.slice.to_sym
30
+ when Prism::CallNode
31
+ # e.g. MyStruct.ptr or MyStruct.by_ref
32
+ node.slice.to_sym
33
+ end
34
+ end
35
+
36
+ # Universal binding extractor for attach_function, sodium_function, etc.
37
+ def extract_function_binding(call_node, method_name:, module_name: nil, default_return: :void)
38
+ return nil unless call_node.is_a?(Prism::CallNode) && call_node.name == method_name
39
+
40
+ all_args = call_node.arguments&.arguments || []
41
+ return nil if all_args.empty?
42
+
43
+ # Strip trailing keyword options hash if present
44
+ pos_args = all_args.grep_v(Prism::KeywordHashNode)
45
+ return nil if pos_args.empty?
46
+
47
+ ruby_name = extract_symbol(pos_args[0])
48
+ return nil if ruby_name.nil?
49
+
50
+ if pos_args.size >= 4
51
+ # Form B: ruby_name, c_name, arg_types, return_type
52
+ c_name = extract_symbol(pos_args[1]) || ruby_name
53
+ args_node = pos_args[2]
54
+ ret_node = pos_args[3]
55
+ elsif pos_args.size == 3
56
+ # Form A: ruby_name, arg_types, return_type
57
+ c_name = ruby_name
58
+ args_node = pos_args[1]
59
+ ret_node = pos_args[2]
60
+ elsif pos_args.size == 2
61
+ c_name = ruby_name
62
+ args_node = nil
63
+ ret_node = pos_args[1]
64
+ else
65
+ return nil
66
+ end
67
+
68
+ arg_types = []
69
+ if args_node.is_a?(Prism::ArrayNode)
70
+ arg_types = (args_node.elements || []).map { |elem| extract_type_symbol(elem) }.compact
71
+ elsif args_node.is_a?(Prism::ConstantReadNode) || args_node.is_a?(Prism::ConstantPathNode)
72
+ arg_types = [args_node.slice.to_sym]
73
+ end
74
+
75
+ return_type = ret_node ? (extract_type_symbol(ret_node) || default_return) : default_return
76
+
77
+ AttachFunctionFact.new(
78
+ ruby_name: ruby_name,
79
+ c_name: c_name,
80
+ arg_types: arg_types,
81
+ return_type: return_type,
82
+ node: call_node,
83
+ receiver_name: module_name
84
+ )
85
+ end
86
+
87
+ def extract_attach_function(call_node, module_name: nil)
88
+ extract_function_binding(call_node, method_name: :attach_function, module_name: module_name, default_return: :void)
89
+ end
90
+
91
+ def ffx_diagnostics_for_call(call_node, path:, target:)
92
+ return [] unless target == :ffx && call_node.is_a?(Prism::CallNode)
93
+
94
+ diagnostics = []
95
+
96
+ case call_node.name
97
+ when :callback
98
+ diagnostics << Rigor::Analysis::Diagnostic.new(
99
+ path: path,
100
+ line: call_node.location.start_line,
101
+ column: call_node.location.start_column + 1,
102
+ rule: "ffx.unsupported-callback",
103
+ message: "callback declarations are not supported by ffx (C extensions use native function pointers)",
104
+ severity: :error
105
+ )
106
+ when :typedef
107
+ diagnostics << Rigor::Analysis::Diagnostic.new(
108
+ path: path,
109
+ line: call_node.location.start_line,
110
+ column: call_node.location.start_column + 1,
111
+ rule: "ffx.unsupported-typedef",
112
+ message: "typedef declarations are not supported by ffx",
113
+ severity: :error
114
+ )
115
+ when :enum, :bitmask
116
+ diagnostics << Rigor::Analysis::Diagnostic.new(
117
+ path: path,
118
+ line: call_node.location.start_line,
119
+ column: call_node.location.start_column + 1,
120
+ rule: "ffx.unsupported-enum",
121
+ message: "#{call_node.name} declarations are not supported by ffx",
122
+ severity: :error
123
+ )
124
+ when :attach_function
125
+ args = call_node.arguments&.arguments || []
126
+ pos_args = args.grep_v(Prism::KeywordHashNode)
127
+
128
+ args_node = pos_args.size >= 4 ? pos_args[2] : pos_args[1]
129
+ ret_node = pos_args.size >= 4 ? pos_args[3] : pos_args[2]
130
+
131
+ if args_node.is_a?(Prism::ArrayNode)
132
+ args_node.elements.each do |elem|
133
+ type_sym = extract_type_symbol(elem)
134
+ if type_sym == :varargs
135
+ diagnostics << Rigor::Analysis::Diagnostic.new(
136
+ path: path,
137
+ line: elem.location.start_line,
138
+ column: elem.location.start_column + 1,
139
+ rule: "ffx.unsupported-varargs",
140
+ message: "varargs are not supported by ffx",
141
+ severity: :error
142
+ )
143
+ elsif type_sym && !Types::FFX_PRIMITIVE_TYPES.include?(type_sym)
144
+ diagnostics << Rigor::Analysis::Diagnostic.new(
145
+ path: path,
146
+ line: elem.location.start_line,
147
+ column: elem.location.start_column + 1,
148
+ rule: "ffx.unsupported-type",
149
+ message: "type :#{type_sym} is not supported by ffx (expected one of 25 primitive types)",
150
+ severity: :error
151
+ )
152
+ end
153
+ end
154
+ end
155
+
156
+ if ret_node
157
+ ret_sym = extract_type_symbol(ret_node)
158
+ if ret_sym && !Types::FFX_PRIMITIVE_TYPES.include?(ret_sym)
159
+ diagnostics << Rigor::Analysis::Diagnostic.new(
160
+ path: path,
161
+ line: ret_node.location.start_line,
162
+ column: ret_node.location.start_column + 1,
163
+ rule: "ffx.unsupported-type",
164
+ message: "return type :#{ret_sym} is not supported by ffx (expected one of 25 primitive types)",
165
+ severity: :error
166
+ )
167
+ end
168
+ end
169
+ end
170
+
171
+ diagnostics
172
+ end
173
+
174
+ def ffx_diagnostics_for_class(class_node, path:, target:)
175
+ return [] unless target == :ffx && class_node.is_a?(Prism::ClassNode)
176
+
177
+ superclass_name = class_node.superclass&.slice
178
+ return [] unless superclass_name
179
+
180
+ if ["FFI::Struct", "::FFI::Struct", "FFI::Union", "::FFI::Union", "FFI::ManagedStruct", "::FFI::ManagedStruct"].include?(superclass_name)
181
+ [
182
+ Rigor::Analysis::Diagnostic.new(
183
+ path: path,
184
+ line: class_node.location.start_line,
185
+ column: class_node.location.start_column + 1,
186
+ rule: "ffx.unsupported-struct",
187
+ message: "FFI::Struct and FFI::Union are not supported by ffx",
188
+ severity: :error
189
+ )
190
+ ]
191
+ else
192
+ []
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ class FFI < Base
6
+ # Fact yielded by a registered BindingRecognizer or emitted from literal attach_function.
7
+ AttachFunctionFact = Data.define(
8
+ :ruby_name,
9
+ :c_name,
10
+ :arg_types,
11
+ :return_type,
12
+ :blocking,
13
+ :node,
14
+ :receiver_name
15
+ ) do
16
+ def initialize(
17
+ ruby_name:,
18
+ c_name: ruby_name,
19
+ arg_types: [],
20
+ return_type: :void,
21
+ blocking: false,
22
+ node: nil,
23
+ receiver_name: nil
24
+ )
25
+ super(
26
+ ruby_name: ruby_name.to_sym,
27
+ c_name: (c_name || ruby_name).to_sym,
28
+ arg_types: Array(arg_types),
29
+ return_type: return_type,
30
+ blocking: blocking,
31
+ node: node,
32
+ receiver_name: receiver_name&.to_s
33
+ )
34
+ end
35
+ end
36
+
37
+ # Extension point for sub-plugins to register custom DSL recognizers (WD2).
38
+ class BindingRecognizer
39
+ attr_reader :name, :block
40
+
41
+ def initialize(name, &block)
42
+ @name = name.to_sym
43
+ @block = block
44
+ end
45
+
46
+ def recognize(node, scope)
47
+ Array(@block.call(node, scope))
48
+ rescue StandardError
49
+ []
50
+ end
51
+ end
52
+
53
+ class << self
54
+ def binding_recognizers
55
+ @binding_recognizers ||= []
56
+ end
57
+
58
+ def register_binding_recognizer(name, &)
59
+ binding_recognizers << BindingRecognizer.new(name, &)
60
+ end
61
+ end
62
+ end
63
+
64
+ class Base
65
+ def self.ffi_binding_recognizers
66
+ @ffi_binding_recognizers ||= []
67
+ end
68
+
69
+ def self.ffi_binding_recognizer(name, &)
70
+ ffi_binding_recognizers << Rigor::Plugin::FFI::BindingRecognizer.new(name, &)
71
+ Rigor::Plugin::FFI.register_binding_recognizer(name, &)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ class FFI < Base
6
+ class FFICatalog
7
+ attr_reader :functions,
8
+ :functions_by_receiver,
9
+ :libraries,
10
+ :structs,
11
+ :typedefs,
12
+ :enums,
13
+ :callbacks,
14
+ :function_method_names,
15
+ :struct_field_names,
16
+ :struct_names,
17
+ :all_receiver_names,
18
+ :all_method_names
19
+
20
+ def initialize(functions: {}, functions_by_receiver: {}, libraries: Set.new, structs: {}, typedefs: {}, enums: {}, callbacks: {})
21
+ @functions = functions
22
+ @functions_by_receiver = functions_by_receiver
23
+ @libraries = libraries
24
+ @structs = structs
25
+ @typedefs = typedefs
26
+ @enums = enums
27
+ @callbacks = callbacks
28
+
29
+ @function_method_names = Set.new(functions.keys.map(&:to_sym)).freeze
30
+
31
+ field_names = Set.new
32
+ structs.each_value do |fields|
33
+ fields.each_key do |f|
34
+ field_names << f.to_sym
35
+ end
36
+ end
37
+ field_names << :[]
38
+ field_names << :[]=
39
+ @struct_field_names = field_names.freeze
40
+ @struct_names = structs.keys.freeze
41
+
42
+ @all_receiver_names = (libraries.to_a + structs.keys).freeze
43
+ @all_method_names = (@function_method_names + @struct_field_names).freeze
44
+ end
45
+
46
+ def function_for(receiver_name, method_name)
47
+ if receiver_name
48
+ scoped = @functions_by_receiver[[receiver_name.to_s, method_name.to_sym]]
49
+ return scoped if scoped
50
+ end
51
+
52
+ # Fallback if receiver matches any registered library
53
+ return unless receiver_name.nil? || @libraries.include?(receiver_name.to_s)
54
+
55
+ @functions[method_name.to_sym]&.first
56
+ end
57
+
58
+ def struct_fields(struct_name)
59
+ @structs[struct_name.to_s]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ class FFI < Base
8
+ class Discoverer
9
+ def self.discover(root:)
10
+ new(root: root).discover
11
+ end
12
+
13
+ def initialize(root:)
14
+ @root = root
15
+ end
16
+
17
+ def discover
18
+ functions = {}
19
+ functions_by_receiver = {}
20
+ libraries = Set.new
21
+ structs = {}
22
+ typedefs = {}
23
+ enums = {}
24
+ callbacks = {}
25
+
26
+ ruby_files.each do |file_path|
27
+ parse_result = Prism.parse_file(file_path)
28
+ next unless parse_result.value
29
+
30
+ walk_tree(parse_result.value, nil) do |node, mod_name|
31
+ case node
32
+ when Prism::CallNode
33
+ handle_call(
34
+ node,
35
+ mod_name,
36
+ functions,
37
+ functions_by_receiver,
38
+ libraries,
39
+ structs,
40
+ typedefs,
41
+ enums,
42
+ callbacks
43
+ )
44
+ when Prism::ClassNode
45
+ handle_class(node, mod_name, structs)
46
+ end
47
+ end
48
+ end
49
+
50
+ FFICatalog.new(
51
+ functions: functions,
52
+ functions_by_receiver: functions_by_receiver,
53
+ libraries: libraries,
54
+ structs: structs,
55
+ typedefs: typedefs,
56
+ enums: enums,
57
+ callbacks: callbacks
58
+ )
59
+ end
60
+
61
+ private
62
+
63
+ def ruby_files
64
+ return [] unless @root && File.directory?(@root)
65
+
66
+ Dir.glob(File.join(@root, "**", "*.rb")).reject { |p| p.include?("/vendor/") || p.include?("/.git/") }
67
+ end
68
+
69
+ def walk_tree(node, current_module, &)
70
+ return unless node
71
+
72
+ case node
73
+ when Prism::ClassNode, Prism::ModuleNode
74
+ name = node.constant_path.slice
75
+ full_name = current_module ? "#{current_module}::#{name}" : name
76
+ yield node, full_name
77
+ node.body&.child_nodes&.each { |child| walk_tree(child, full_name, &) }
78
+ else
79
+ yield node, current_module
80
+ node.child_nodes.each { |child| walk_tree(child, current_module, &) }
81
+ end
82
+ end
83
+
84
+ def handle_call(node, mod_name, functions, functions_by_receiver, libraries, structs, typedefs, enums, callbacks)
85
+ case node.name
86
+ when :extend
87
+ arg = node.arguments&.arguments&.first
88
+ if arg && ["FFI::Library", "::FFI::Library", "FFX::Library", "::FFX::Library"].include?(arg.slice) && mod_name
89
+ libraries << mod_name
90
+ end
91
+ when :attach_function
92
+ fact = Analyzer.extract_attach_function(node, module_name: mod_name)
93
+ if fact
94
+ (functions[fact.ruby_name] ||= []) << fact
95
+ if mod_name
96
+ libraries << mod_name
97
+ functions_by_receiver[[mod_name, fact.ruby_name]] = fact
98
+ end
99
+ end
100
+ when :callback
101
+ # Issue 4 fix: extract callback definitions
102
+ args = node.arguments&.arguments || []
103
+ if args.size >= 3
104
+ cb_name = Analyzer.extract_symbol(args[0])
105
+ cb_params = []
106
+ if args[1].is_a?(Prism::ArrayNode)
107
+ cb_params = (args[1].elements || []).map { |elem| Analyzer.extract_type_symbol(elem) }.compact
108
+ end
109
+ cb_ret = Analyzer.extract_type_symbol(args[2]) || :void
110
+ if cb_name
111
+ callbacks[cb_name] = { params: cb_params, return_type: cb_ret }
112
+ typedefs[cb_name] = :pointer
113
+ libraries << mod_name if mod_name
114
+ end
115
+ end
116
+ when :typedef
117
+ args = node.arguments&.arguments || []
118
+ if args.size >= 2
119
+ old_t = Analyzer.extract_type_symbol(args[0])
120
+ new_t = Analyzer.extract_symbol(args[1])
121
+ typedefs[new_t] = old_t if old_t && new_t
122
+ libraries << mod_name if mod_name
123
+ end
124
+ when :enum, :bitmask
125
+ args = node.arguments&.arguments || []
126
+ if args.size >= 2
127
+ enum_name = Analyzer.extract_symbol(args[0])
128
+ enums[enum_name] = true if enum_name
129
+ libraries << mod_name if mod_name
130
+ end
131
+ when :layout
132
+ # Struct layout definition
133
+ if mod_name
134
+ fields = structs[mod_name] ||= {}
135
+ args = node.arguments&.arguments || []
136
+ i = 0
137
+ while i < args.size
138
+ f_name = Analyzer.extract_symbol(args[i])
139
+ f_type = Analyzer.extract_type_symbol(args[i + 1])
140
+ fields[f_name] = f_type if f_name && f_type
141
+ i += 2
142
+ end
143
+ end
144
+ else
145
+ # Issue 5 fix: pass mod_name to sub-plugin recognizers
146
+ FFI.binding_recognizers.each do |recognizer|
147
+ facts = recognizer.recognize(node, mod_name)
148
+ Array(facts).each do |fact|
149
+ next unless fact.is_a?(AttachFunctionFact)
150
+
151
+ (functions[fact.ruby_name] ||= []) << fact
152
+ rec = fact.receiver_name || mod_name
153
+ if rec
154
+ libraries << rec
155
+ functions_by_receiver[[rec, fact.ruby_name]] = fact
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def handle_class(node, mod_name, structs)
163
+ superclass_name = node.superclass&.slice
164
+ return unless superclass_name
165
+
166
+ return unless ["FFI::Struct", "::FFI::Struct", "FFI::Union", "::FFI::Union"].include?(superclass_name)
167
+
168
+ structs[mod_name] ||= {} if mod_name
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end