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,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ class FFI < Base
6
+ module TargetDetector
7
+ module_function
8
+
9
+ # Detects whether the project targets ffx or the classic ffi gem per WD6.
10
+ def detect(root:, config: {})
11
+ explicit = config["target"] || config[:target]
12
+ return explicit.to_sym if explicit && explicit != "auto"
13
+
14
+ return :ffx if extconf_uses_ffx?(root)
15
+ return :ffx if lockfile_has_ffx?(root)
16
+
17
+ :ffi
18
+ end
19
+
20
+ def extconf_uses_ffx?(root)
21
+ return false if root.nil?
22
+
23
+ Dir.glob(File.join(root.to_s, "ext/**/extconf.rb")).any? do |extconf|
24
+ File.read(extconf).include?("FFX.create_makefile")
25
+ rescue SystemCallError
26
+ false
27
+ end
28
+ end
29
+
30
+ def lockfile_has_ffx?(root)
31
+ return false if root.nil?
32
+
33
+ lockfile = File.join(root.to_s, "Gemfile.lock")
34
+ return false unless File.file?(lockfile)
35
+
36
+ File.foreach(lockfile) do |line|
37
+ return true if line =~ /^\s{4}ffx\s/
38
+ end
39
+ false
40
+ rescue SystemCallError
41
+ false
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ class FFI < Base
6
+ module Types
7
+ # The 25 primitive type symbols supported by ffx and classic FFI
8
+ FFX_PRIMITIVE_TYPES = %i[
9
+ void
10
+ int uint short ushort long ulong
11
+ int8 int16 int32 int64
12
+ uint8 uint16 uint32 uint64
13
+ long_long ulong_long
14
+ size_t
15
+ float double
16
+ bool
17
+ char uchar
18
+ string
19
+ pointer
20
+ ].to_set.freeze
21
+
22
+ # Regex anchor must match end of string \z
23
+ NOMINAL_OPAQUE_REGEX = Regexp.new("(_ptr|_handle|Ptr|Handle)\\z")
24
+
25
+ BOOL_TYPE = Rigor::Type::Combinator.union(
26
+ Rigor::Type::Combinator.constant_of(true),
27
+ Rigor::Type::Combinator.constant_of(false)
28
+ ).freeze
29
+
30
+ NIL_TYPE = Rigor::Type::Combinator.constant_of(nil)
31
+
32
+ module_function
33
+
34
+ def nominal_opaque_pointer?(alias_name, exceptions: [])
35
+ sym_str = alias_name.to_s
36
+ return false if Array(exceptions).map(&:to_s).include?(sym_str)
37
+
38
+ NOMINAL_OPAQUE_REGEX.match?(sym_str)
39
+ end
40
+
41
+ def camelize(name)
42
+ name.to_s.split("_").map(&:capitalize).join
43
+ end
44
+
45
+ def return_type_for(type_sym, target: :ffi, module_name: nil, exceptions: [], typedefs: {}, callbacks: {})
46
+ sym = type_sym.is_a?(Symbol) ? type_sym : type_sym.to_s.to_sym
47
+
48
+ # Check if symbol is a defined callback
49
+ if callbacks.key?(sym)
50
+ return target == :ffx ? Rigor::Type::Combinator.nominal_of("Integer") : Rigor::Type::Combinator.nominal_of("FFI::Function")
51
+ end
52
+
53
+ if typedefs.key?(sym)
54
+ underlying = typedefs[sym]
55
+ if underlying == :pointer && nominal_opaque_pointer?(sym, exceptions: exceptions)
56
+ nominal = camelize(sym)
57
+ nominal = "#{module_name}::#{nominal}" if module_name && !module_name.empty?
58
+ return Rigor::Type::Combinator.nominal_of(nominal)
59
+ end
60
+ sym = underlying
61
+ end
62
+
63
+ case sym
64
+ when :void
65
+ NIL_TYPE
66
+ when :int, :uint, :short, :ushort, :long, :ulong,
67
+ :int8, :int16, :int32, :int64,
68
+ :uint8, :uint16, :uint32, :uint64,
69
+ :long_long, :ulong_long, :size_t, :char, :uchar
70
+ Rigor::Type::Combinator.nominal_of("Integer")
71
+ when :float, :double
72
+ Rigor::Type::Combinator.nominal_of("Float")
73
+ when :bool
74
+ BOOL_TYPE
75
+ when :string
76
+ Rigor::Type::Combinator.nominal_of("String")
77
+ when :pointer
78
+ if target == :ffx
79
+ Rigor::Type::Combinator.nominal_of("Integer")
80
+ else
81
+ Rigor::Type::Combinator.nominal_of("FFI::Pointer")
82
+ end
83
+ else
84
+ if nominal_opaque_pointer?(sym, exceptions: exceptions)
85
+ nominal = camelize(sym)
86
+ nominal = "#{module_name}::#{nominal}" if module_name && !module_name.empty?
87
+ Rigor::Type::Combinator.nominal_of(nominal)
88
+ else
89
+ Rigor::Type::Combinator.top
90
+ end
91
+ end
92
+ end
93
+
94
+ # Universal parameter widening per WD7
95
+ def param_type_for(type_sym, target: :ffi, module_name: nil, exceptions: [], typedefs: {}, callbacks: {})
96
+ sym = type_sym.is_a?(Symbol) ? type_sym : type_sym.to_s.to_sym
97
+
98
+ if callbacks.key?(sym)
99
+ return Rigor::Type::Combinator.union(
100
+ Rigor::Type::Combinator.nominal_of("Proc"),
101
+ Rigor::Type::Combinator.nominal_of("Method"),
102
+ Rigor::Type::Combinator.nominal_of("FFI::Pointer"),
103
+ Rigor::Type::Combinator.nominal_of("FFI::Function"),
104
+ NIL_TYPE
105
+ )
106
+ end
107
+
108
+ if typedefs.key?(sym)
109
+ underlying = typedefs[sym]
110
+ if underlying == :pointer && nominal_opaque_pointer?(sym, exceptions: exceptions)
111
+ nominal = camelize(sym)
112
+ nominal = "#{module_name}::#{nominal}" if module_name && !module_name.empty?
113
+ return Rigor::Type::Combinator.union(
114
+ Rigor::Type::Combinator.nominal_of(nominal),
115
+ Rigor::Type::Combinator.nominal_of("FFI::Pointer"),
116
+ NIL_TYPE
117
+ )
118
+ end
119
+ sym = underlying
120
+ end
121
+
122
+ case sym
123
+ when :pointer
124
+ if target == :ffx
125
+ Rigor::Type::Combinator.union(
126
+ Rigor::Type::Combinator.nominal_of("Integer"),
127
+ NIL_TYPE
128
+ )
129
+ else
130
+ Rigor::Type::Combinator.union(
131
+ Rigor::Type::Combinator.nominal_of("FFI::Pointer"),
132
+ Rigor::Type::Combinator.nominal_of("FFI::MemoryPointer"),
133
+ Rigor::Type::Combinator.nominal_of("FFI::AutoPointer"),
134
+ Rigor::Type::Combinator.nominal_of("FFI::Buffer"),
135
+ Rigor::Type::Combinator.nominal_of("Integer"),
136
+ Rigor::Type::Combinator.nominal_of("String"),
137
+ NIL_TYPE
138
+ )
139
+ end
140
+ when :int, :uint, :short, :ushort, :long, :ulong,
141
+ :int8, :int16, :int32, :int64,
142
+ :uint8, :uint16, :uint32, :uint64,
143
+ :long_long, :ulong_long, :size_t, :char, :uchar
144
+ Rigor::Type::Combinator.nominal_of("Integer")
145
+ when :float, :double
146
+ Rigor::Type::Combinator.union(
147
+ Rigor::Type::Combinator.nominal_of("Float"),
148
+ Rigor::Type::Combinator.nominal_of("Integer")
149
+ )
150
+ when :bool
151
+ BOOL_TYPE
152
+ when :string
153
+ Rigor::Type::Combinator.nominal_of("String")
154
+ else
155
+ Rigor::Type::Combinator.top
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/plugin"
4
+ require_relative "ffi/binding_recognizer"
5
+ require_relative "ffi/types"
6
+ require_relative "ffi/target_detector"
7
+ require_relative "ffi/analyzer"
8
+ require_relative "ffi/catalog"
9
+ require_relative "ffi/discoverer"
10
+
11
+ module Rigor
12
+ module Plugin
13
+ class FFI < Base
14
+ manifest(
15
+ id: "ffi",
16
+ version: "0.1.0",
17
+ description: "Models FFI library bindings, struct layouts, callbacks, carrier types, and ffx target compatibility.",
18
+ signature_paths: ["sig"]
19
+ )
20
+
21
+ producer :ffi_catalog do
22
+ root = config["root"] || Dir.pwd
23
+ Discoverer.discover(root: root)
24
+ end
25
+
26
+ # Dynamic return rule for attached FFI functions
27
+ # Gated on known FFI library receivers and attached function names
28
+ dynamic_return receivers: -> { producer_value(:ffi_catalog)&.libraries || [] },
29
+ methods: -> { producer_value(:ffi_catalog)&.function_method_names || [] } do |call_node, scope|
30
+ catalog = producer_value(:ffi_catalog)
31
+ next nil if catalog.nil?
32
+
33
+ receiver_type = call_node.receiver ? scope&.type_of(call_node.receiver) : scope&.self_type
34
+ receiver_class = case receiver_type
35
+ when Rigor::Type::Nominal, Rigor::Type::Singleton then receiver_type.class_name
36
+ end
37
+ next nil if receiver_class.nil?
38
+
39
+ fact = catalog.function_for(receiver_class, call_node.name)
40
+ next nil if fact.nil?
41
+
42
+ Types.return_type_for(
43
+ fact.return_type,
44
+ target: @target || :ffi,
45
+ module_name: fact.receiver_name,
46
+ exceptions: @exceptions || [],
47
+ typedefs: catalog.typedefs,
48
+ callbacks: catalog.callbacks
49
+ )
50
+ end
51
+
52
+ # Dynamic return rule for FFI struct field accessors
53
+ # Gated on known FFI struct class names and field names
54
+ dynamic_return receivers: -> { producer_value(:ffi_catalog)&.struct_names || [] },
55
+ methods: -> { producer_value(:ffi_catalog)&.struct_field_names || [] } do |call_node, scope|
56
+ catalog = producer_value(:ffi_catalog)
57
+ next nil if catalog.nil?
58
+
59
+ receiver_type = call_node.receiver ? scope&.type_of(call_node.receiver) : scope&.self_type
60
+ receiver_class = case receiver_type
61
+ when Rigor::Type::Nominal, Rigor::Type::Singleton then receiver_type.class_name
62
+ end
63
+ next nil if receiver_class.nil?
64
+
65
+ fields = catalog.struct_fields(receiver_class)
66
+ next nil if fields.nil?
67
+
68
+ if call_node.name == :[]
69
+ arg = call_node.arguments&.arguments&.first
70
+ field_sym = Analyzer.extract_symbol(arg)
71
+ field_type = fields[field_sym]
72
+ next nil if field_type.nil?
73
+
74
+ Types.return_type_for(
75
+ field_type,
76
+ target: @target || :ffi,
77
+ module_name: receiver_class,
78
+ exceptions: @exceptions || [],
79
+ typedefs: catalog.typedefs,
80
+ callbacks: catalog.callbacks
81
+ )
82
+ elsif call_node.name.to_s.end_with?("=")
83
+ # For both .id = val and [:id] = val, the assigned value is the LAST argument
84
+ val_node = call_node.arguments&.arguments&.last
85
+ val_node ? scope&.type_of(val_node) : Rigor::Type::Combinator.top
86
+ elsif (field_type = fields[call_node.name])
87
+ Types.return_type_for(
88
+ field_type,
89
+ target: @target || :ffi,
90
+ module_name: receiver_class,
91
+ exceptions: @exceptions || [],
92
+ typedefs: catalog.typedefs,
93
+ callbacks: catalog.callbacks
94
+ )
95
+ end
96
+ end
97
+
98
+ node_rule Prism::CallNode do |node, _scope, path|
99
+ Analyzer.ffx_diagnostics_for_call(node, path: path, target: @target || :ffi)
100
+ end
101
+
102
+ node_rule Prism::ClassNode do |node, _scope, path|
103
+ Analyzer.ffx_diagnostics_for_class(node, path: path, target: @target || :ffi)
104
+ end
105
+
106
+ def init(_services)
107
+ root = config["root"] || Dir.pwd
108
+ @target = TargetDetector.detect(root: root, config: config)
109
+ @exceptions = config["exceptions"] || []
110
+ end
111
+
112
+ def prepare(_services)
113
+ root = config["root"] || Dir.pwd
114
+ @target = TargetDetector.detect(root: root, config: config)
115
+ @exceptions = config["exceptions"] || []
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ Rigor::Plugin.register(Rigor::Plugin::FFI)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rigor/plugin/ffi"
@@ -0,0 +1,67 @@
1
+ module FFI
2
+ class Pointer
3
+ NULL: Pointer
4
+ def self.new: (Integer address) -> instance
5
+ def null?: () -> bool
6
+ def address: () -> Integer
7
+ def +: (Integer offset) -> Pointer
8
+ def read_string: (?Integer? length) -> String
9
+ def read_bytes: (Integer length) -> String
10
+ def write_string: (String str, ?Integer? length) -> Pointer
11
+ def write_bytes: (String str, ?Integer offset, ?Integer? length) -> Pointer
12
+ end
13
+
14
+ class MemoryPointer < Pointer
15
+ def self.new: (Symbol | Integer type, ?Integer count, ?bool clear) -> instance
16
+ def self.from_string: (String s) -> instance
17
+ end
18
+
19
+ class AutoPointer < Pointer
20
+ def self.new: (Pointer ptr, ?(Method | Proc | Symbol)? releaser) -> instance
21
+ def free: () -> void
22
+ def autorelease=: (bool val) -> bool
23
+ end
24
+
25
+ class Buffer < Pointer
26
+ def self.new: (Symbol | Integer type, ?Integer size, ?Integer count) -> instance
27
+ end
28
+
29
+ class Function < Pointer
30
+ def self.new: (Symbol return_type, Array[Symbol] param_types, ?Hash[Symbol, untyped] options) ?{ (*untyped) -> untyped } -> instance
31
+ def call: (*untyped) -> untyped
32
+ end
33
+
34
+ class Struct
35
+ def self.layout: (*untyped) -> void
36
+ def self.by_ref: () -> untyped
37
+ def self.ptr: () -> untyped
38
+ def self.size: () -> Integer
39
+ def pointer: () -> Pointer
40
+ def to_ptr: () -> Pointer
41
+ def null?: () -> bool
42
+ def []: (Symbol | String name) -> untyped
43
+ def []=: (Symbol | String name, untyped value) -> untyped
44
+ end
45
+
46
+ class Union < Struct
47
+ end
48
+
49
+ class ManagedStruct < Struct
50
+ end
51
+
52
+ module Library
53
+ def ffi_lib: (*(String | Array[String]) libs) -> untyped
54
+ def attach_function: (*untyped) -> untyped
55
+ def callback: (Symbol name, Array[Symbol] param_types, Symbol return_type) -> untyped
56
+ def typedef: (Symbol | String old_type, Symbol | String new_type) -> untyped
57
+ def enum: (*untyped) -> untyped
58
+ def bitmask: (*untyped) -> untyped
59
+ end
60
+ end
61
+
62
+ module FFX
63
+ module Library
64
+ def ffi_lib: (*(String | Array[String]) libs) -> untyped
65
+ def attach_function: (*untyped) -> untyped
66
+ end
67
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/plugin"
4
+ require "rigor-ffi"
5
+
6
+ module Rigor
7
+ module Plugin
8
+ class FFIRZMQ < Base
9
+ manifest(
10
+ id: "ffi-rzmq",
11
+ version: "0.1.0",
12
+ description: "Models ffi-rzmq / LibZMQ wrappers, socket operations, and cross-gem bindings.",
13
+ signature_paths: ["sig"]
14
+ )
15
+ end
16
+ end
17
+ end
18
+
19
+ Rigor::Plugin.register(Rigor::Plugin::FFIRZMQ)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rigor/plugin/ffi_rzmq"
@@ -0,0 +1,29 @@
1
+ module ZMQ
2
+ REQ: Integer
3
+ REP: Integer
4
+ PUB: Integer
5
+ SUB: Integer
6
+ DONTWAIT: Integer
7
+ SNDMORE: Integer
8
+
9
+ class Context
10
+ def self.new: (?Integer io_threads) -> instance
11
+ def socket: (Integer type) -> Socket?
12
+ def terminate: () -> Integer
13
+ end
14
+
15
+ class Socket
16
+ def connect: (String endpoint) -> Integer
17
+ def bind: (String endpoint) -> Integer
18
+ def send_string: (String message, ?Integer flags) -> Integer
19
+ def recv_string: (String message, ?Integer flags) -> Integer
20
+ def close: () -> Integer
21
+ end
22
+ end
23
+
24
+ module LibZMQ
25
+ def self.zmq_ctx_new: () -> FFI::Pointer
26
+ def self.zmq_socket: (FFI::Pointer context, Integer type) -> FFI::Pointer
27
+ def self.zmq_close: (FFI::Pointer socket) -> Integer
28
+ def self.zmq_msg_init: (FFI::Pointer msg) -> Integer
29
+ end
@@ -254,7 +254,7 @@ module Rigor
254
254
  # Mirror of `extract_nullability` but reads the `required:` kwarg, defaulting to `false`
255
255
  # (graphql-ruby's argument default — the OPPOSITE polarity of `field`'s `null:` / nullability
256
256
  # default).
257
- # rubocop:disable Naming/PredicateMethod -- extractor returns the literal required value
257
+ # rubocop:disable-next Naming/PredicateMethod -- extractor returns the literal required value
258
258
  def extract_required_flag(args)
259
259
  kwargs = args.last
260
260
  return false unless kwargs.is_a?(Prism::KeywordHashNode)
@@ -270,7 +270,6 @@ module Rigor
270
270
  else false
271
271
  end
272
272
  end
273
- # rubocop:enable Naming/PredicateMethod
274
273
  private_class_method :extract_required_flag
275
274
 
276
275
  # `field :name, Type, null: false` shape. The first positional is a Symbol (field name); the
@@ -331,7 +330,7 @@ module Rigor
331
330
 
332
331
  # Defaults to `true` (matches graphql-ruby's `field` default nullability). Looks for an explicit
333
332
  # `null:` keyword and reads its boolean literal.
334
- # rubocop:disable Naming/PredicateMethod -- extractor returns the literal nullability value
333
+ # rubocop:disable-next Naming/PredicateMethod -- extractor returns the literal nullability value
335
334
  def extract_nullability(args)
336
335
  kwargs = args.last
337
336
  return true unless kwargs.is_a?(Prism::KeywordHashNode)
@@ -347,7 +346,6 @@ module Rigor
347
346
  else true
348
347
  end
349
348
  end
350
- # rubocop:enable Naming/PredicateMethod
351
349
  private_class_method :extract_nullability
352
350
 
353
351
  # Returns the constant chain as an Array of String segments (`["GraphQL", "Schema", "Object"]`).
@@ -69,7 +69,8 @@ module Rigor
69
69
  def ruby_files_under(roots)
70
70
  roots.flat_map do |root|
71
71
  absolute = File.expand_path(root)
72
- next [] unless File.directory?(absolute)
72
+ # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
73
+ next [] unless @io_boundary.directory?(absolute)
73
74
 
74
75
  Dir.glob(File.join(absolute, "**", "*.rb"))
75
76
  end
@@ -58,7 +58,8 @@ module Rigor
58
58
  def ruby_files_under(roots)
59
59
  roots.flat_map do |root|
60
60
  absolute = File.expand_path(root)
61
- next [] unless File.directory?(absolute)
61
+ # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
62
+ next [] unless @io_boundary.directory?(absolute)
62
63
 
63
64
  Dir.glob(File.join(absolute, "**", "*.rb"))
64
65
  end
@@ -88,7 +88,8 @@ module Rigor
88
88
  def locale_files
89
89
  @search_paths.flat_map do |root|
90
90
  absolute = File.expand_path(root)
91
- next [] unless File.directory?(absolute)
91
+ # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
92
+ next [] unless @io_boundary.directory?(absolute)
92
93
 
93
94
  Dir.glob(File.join(absolute, "**", "*.{yml,yaml}"))
94
95
  end.sort
@@ -177,7 +177,8 @@ module Rigor
177
177
  def view_files
178
178
  @view_search_paths.flat_map do |root|
179
179
  absolute = File.expand_path(root)
180
- next [] unless File.directory?(absolute)
180
+ # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
181
+ next [] unless io_boundary.directory?(absolute)
181
182
 
182
183
  Dir.glob(File.join(absolute, "**", "*.{erb,haml,slim}"))
183
184
  end.sort
@@ -164,7 +164,8 @@ module Rigor
164
164
  def each_ruby_file(dirs, &)
165
165
  dirs.each do |dir|
166
166
  absolute = File.expand_path(dir)
167
- next unless File.directory?(absolute)
167
+ # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
168
+ next unless io_boundary.directory?(absolute)
168
169
 
169
170
  Dir.glob(File.join(absolute, "**", "*.rb")).each(&)
170
171
  end