rigortype 0.1.2 → 0.1.4

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +135 -31
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +113 -0
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +72 -0
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +139 -0
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +200 -0
  9. data/lib/rigor/analysis/dependency_source_inference.rb +38 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +11 -3
  12. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  13. data/lib/rigor/analysis/runner.rb +206 -6
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +59 -6
  16. data/lib/rigor/cache/store.rb +1 -1
  17. data/lib/rigor/cli/diff_command.rb +1 -1
  18. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  19. data/lib/rigor/cli/type_of_command.rb +1 -1
  20. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  21. data/lib/rigor/cli/type_scan_report.rb +2 -2
  22. data/lib/rigor/cli.rb +9 -1
  23. data/lib/rigor/configuration/dependencies.rb +235 -0
  24. data/lib/rigor/configuration.rb +45 -11
  25. data/lib/rigor/environment.rb +47 -4
  26. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  27. data/lib/rigor/flow_contribution/element.rb +1 -1
  28. data/lib/rigor/flow_contribution/fact.rb +1 -1
  29. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  30. data/lib/rigor/flow_contribution/merger.rb +7 -3
  31. data/lib/rigor/flow_contribution.rb +2 -2
  32. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  33. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  34. data/lib/rigor/inference/expression_typer.rb +67 -11
  35. data/lib/rigor/inference/fallback.rb +1 -1
  36. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  37. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
  38. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  39. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  40. data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
  41. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
  42. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  43. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
  44. data/lib/rigor/inference/method_dispatcher.rb +233 -2
  45. data/lib/rigor/inference/method_parameter_binder.rb +1 -3
  46. data/lib/rigor/inference/narrowing.rb +2 -4
  47. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  48. data/lib/rigor/inference/scope_indexer.rb +14 -9
  49. data/lib/rigor/inference/statement_evaluator.rb +70 -6
  50. data/lib/rigor/plugin/io_boundary.rb +0 -2
  51. data/lib/rigor/plugin/loader.rb +2 -2
  52. data/lib/rigor/plugin/manifest.rb +49 -7
  53. data/lib/rigor/plugin/registry.rb +11 -0
  54. data/lib/rigor/plugin/services.rb +1 -1
  55. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  56. data/lib/rigor/plugin.rb +1 -0
  57. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  58. data/lib/rigor/rbs_extended.rb +131 -32
  59. data/lib/rigor/scope.rb +25 -8
  60. data/lib/rigor/sig_gen/classification.rb +36 -0
  61. data/lib/rigor/sig_gen/generator.rb +1048 -0
  62. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  63. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  64. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  65. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  66. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  67. data/lib/rigor/sig_gen/renderer.rb +157 -0
  68. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  69. data/lib/rigor/sig_gen/write_result.rb +48 -0
  70. data/lib/rigor/sig_gen/writer.rb +530 -0
  71. data/lib/rigor/sig_gen.rb +25 -0
  72. data/lib/rigor/type/bound_method.rb +79 -0
  73. data/lib/rigor/type/combinator.rb +195 -2
  74. data/lib/rigor/type/constant.rb +13 -0
  75. data/lib/rigor/type/hash_shape.rb +0 -2
  76. data/lib/rigor/type/union.rb +20 -1
  77. data/lib/rigor/type.rb +1 -0
  78. data/lib/rigor/type_node/generic.rb +62 -0
  79. data/lib/rigor/type_node/identifier.rb +30 -0
  80. data/lib/rigor/type_node/indexed_access.rb +41 -0
  81. data/lib/rigor/type_node/integer_literal.rb +29 -0
  82. data/lib/rigor/type_node/name_scope.rb +52 -0
  83. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  84. data/lib/rigor/type_node/string_literal.rb +29 -0
  85. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  86. data/lib/rigor/type_node/union.rb +42 -0
  87. data/lib/rigor/type_node.rb +29 -0
  88. data/lib/rigor/version.rb +1 -1
  89. data/lib/rigor.rb +2 -0
  90. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  91. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  92. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  93. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  94. data/sig/rigor/cli/diff_command.rbs +4 -0
  95. data/sig/rigor/cli/explain_command.rbs +4 -0
  96. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  97. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  98. data/sig/rigor/environment.rbs +6 -2
  99. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  100. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  101. data/sig/rigor/inference/builtins.rbs +2 -0
  102. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  103. data/sig/rigor/plugin/base.rbs +6 -0
  104. data/sig/rigor/plugin/fact_store.rbs +11 -0
  105. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  106. data/sig/rigor/plugin/load_error.rbs +6 -0
  107. data/sig/rigor/plugin/loader.rbs +20 -0
  108. data/sig/rigor/plugin/manifest.rbs +9 -0
  109. data/sig/rigor/plugin/registry.rbs +3 -0
  110. data/sig/rigor/plugin/services.rbs +3 -0
  111. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  112. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  113. data/sig/rigor/plugin.rbs +8 -0
  114. data/sig/rigor/scope.rbs +4 -2
  115. data/sig/rigor/type.rbs +28 -6
  116. metadata +58 -1
@@ -6,12 +6,14 @@ require_relative "../environment"
6
6
  require_relative "../scope"
7
7
  require_relative "../cache/store"
8
8
  require_relative "../plugin"
9
+ require_relative "../rbs_extended/reporter"
9
10
  require_relative "../reflection"
10
11
  require_relative "../type/combinator"
11
12
  require_relative "../inference/coverage_scanner"
12
13
  require_relative "../inference/scope_indexer"
13
14
  require_relative "../inference/method_dispatcher/file_folding"
14
15
  require_relative "check_rules"
16
+ require_relative "dependency_source_inference"
15
17
  require_relative "diagnostic"
16
18
  require_relative "result"
17
19
 
@@ -21,7 +23,8 @@ module Rigor
21
23
  RUBY_GLOB = "**/*.rb"
22
24
  DEFAULT_CACHE_ROOT = ".rigor/cache"
23
25
 
24
- attr_reader :cache_store, :plugin_registry
26
+ attr_reader :cache_store, :plugin_registry, :dependency_source_index,
27
+ :rbs_extended_reporter, :boundary_cross_reporter
25
28
 
26
29
  # @param configuration [Rigor::Configuration]
27
30
  # @param explain [Boolean] surface fail-soft fallback events
@@ -40,6 +43,9 @@ module Rigor
40
43
  @cache_store = cache_store
41
44
  @plugin_requirer = plugin_requirer
42
45
  @plugin_registry = Plugin::Registry::EMPTY
46
+ @dependency_source_index = DependencySourceInference::Index::EMPTY
47
+ @rbs_extended_reporter = RbsExtended::Reporter.new
48
+ @boundary_cross_reporter = DependencySourceInference::BoundaryCrossReporter.new
43
49
  end
44
50
 
45
51
  # Walks every Ruby file under `paths`, parses it, builds a
@@ -58,22 +64,41 @@ module Rigor
58
64
  return Result.new(diagnostics: [target_ruby_error]) if target_ruby_error
59
65
 
60
66
  @plugin_registry = load_plugins
67
+ @dependency_source_index = DependencySourceInference::Builder.build(@configuration.dependencies)
61
68
  environment = Environment.for_project(
62
69
  libraries: @configuration.libraries,
63
70
  signature_paths: @configuration.signature_paths,
64
71
  cache_store: @cache_store,
65
- plugin_registry: @plugin_registry
72
+ plugin_registry: @plugin_registry,
73
+ dependency_source_index: @dependency_source_index,
74
+ rbs_extended_reporter: @rbs_extended_reporter,
75
+ boundary_cross_reporter: @boundary_cross_reporter
66
76
  )
67
77
  expansion = expand_paths(paths)
68
78
 
69
- diagnostics = plugin_load_diagnostics
70
- diagnostics += plugin_prepare_diagnostics
71
- diagnostics += expansion.fetch(:errors)
79
+ diagnostics = pre_file_diagnostics(expansion)
72
80
  diagnostics += expansion.fetch(:files).flat_map { |path| analyze_file(path, environment) }
81
+ diagnostics += rbs_extended_reporter_diagnostics
82
+ diagnostics += boundary_cross_diagnostics
73
83
 
74
84
  Result.new(diagnostics: apply_severity_profile(diagnostics))
75
85
  end
76
86
 
87
+ # Pre-file diagnostic streams that fire once per run rather
88
+ # than per analyzed file: plugin load / prepare envelopes,
89
+ # the ADR-10 dependency-source resolution surface, and the
90
+ # `expand_paths` errors for `paths:` entries that don't
91
+ # exist or aren't `.rb`. Aggregated here so `#run` stays
92
+ # under the ABC budget.
93
+ def pre_file_diagnostics(expansion)
94
+ plugin_load_diagnostics +
95
+ plugin_prepare_diagnostics +
96
+ dependency_source_diagnostics +
97
+ dependency_source_budget_diagnostics +
98
+ dependency_source_config_conflict_diagnostics +
99
+ expansion.fetch(:errors)
100
+ end
101
+
77
102
  # `target_ruby` flows through to Prism's `version:` option.
78
103
  # Prism enforces the supported range and raises
79
104
  # `ArgumentError` for versions it does not recognise. Run a
@@ -207,6 +232,181 @@ module Rigor
207
232
  end
208
233
  end
209
234
 
235
+ # ADR-10 § "Diagnostic prefix family" — surfaces gems
236
+ # listed in `dependencies.source_inference` that RubyGems
237
+ # could not resolve. The run continues; the gem simply
238
+ # contributes nothing this session, mirroring the
239
+ # plugin-load error envelope. Authored `:warning` because
240
+ # an unresolvable gem usually means a typo or a missing
241
+ # `bundle install` rather than a project-blocking problem;
242
+ # the severity profile still re-stamps it.
243
+ def dependency_source_diagnostics
244
+ @dependency_source_index.unresolvable.map do |entry|
245
+ Diagnostic.new(
246
+ path: ".rigor.yml",
247
+ line: 1,
248
+ column: 1,
249
+ message: "dependencies.source_inference[].gem #{entry.gem_name.inspect} could not be " \
250
+ "resolved (#{entry.reason}); skipping",
251
+ severity: :warning,
252
+ rule: "dynamic.dependency-source.gem-not-found",
253
+ source_family: :builtin
254
+ )
255
+ end
256
+ end
257
+
258
+ # ADR-10 § "Budget interaction" / slice 4 — emits one
259
+ # `:warning` per gem whose Walker run hit the
260
+ # `dependencies.budget_per_gem` cap. The cap is a Walker-
261
+ # side guard rail (slice 4 picks the (α) semantics from
262
+ # ADR-10 WD4: harvesting stops, the dispatcher behaves
263
+ # exactly as before for unrecorded methods). The
264
+ # diagnostic names the gem and points the user at the
265
+ # three remediations: ship RBS, reduce `mode:` from
266
+ # `full` to `when_missing`, or de-list the gem.
267
+ # ADR-10 § "config-conflict diagnostic" / 5d — surfaces
268
+ # `Configuration::Dependencies` warnings accumulated
269
+ # during `from_h` deduplication of the `includes:`-chain
270
+ # source_inference array. Each warning describes a
271
+ # per-gem mode conflict that the merge resolved
272
+ # right-wins; the user sees one diagnostic per conflict.
273
+ # `:warning` matches the user's "warn but don't block"
274
+ # preference per the design discussion.
275
+ def dependency_source_config_conflict_diagnostics
276
+ @configuration.dependencies.warnings.map do |message|
277
+ Diagnostic.new(
278
+ path: ".rigor.yml",
279
+ line: 1,
280
+ column: 1,
281
+ message: message,
282
+ severity: :warning,
283
+ rule: "dynamic.dependency-source.config-conflict",
284
+ source_family: :builtin
285
+ )
286
+ end
287
+ end
288
+
289
+ def dependency_source_budget_diagnostics
290
+ budget = @configuration.dependencies.budget_per_gem
291
+ @dependency_source_index.budget_exceeded.map do |gem_name|
292
+ Diagnostic.new(
293
+ path: ".rigor.yml",
294
+ line: 1,
295
+ column: 1,
296
+ message: "dependencies.source_inference[].gem #{gem_name.inspect} exceeded the per-gem " \
297
+ "catalog cap (#{budget} method definitions); the remaining methods fall back " \
298
+ "to the existing RBS-or-Dynamic[top] boundary. Ship RBS for the gem, set " \
299
+ "`mode: when_missing` instead of `full`, or de-list the gem.",
300
+ severity: :warning,
301
+ rule: "dynamic.dependency-source.budget-exceeded",
302
+ source_family: :builtin
303
+ )
304
+ end
305
+ end
306
+
307
+ # ADR-13 slice 3b — drains the per-run
308
+ # {RbsExtended::Reporter} into one diagnostic per accumulated
309
+ # event:
310
+ #
311
+ # - `dynamic.rbs-extended.unresolved` for every annotation
312
+ # payload the parser could not turn into a {Rigor::Type}.
313
+ # Surfaces typos and references to plugin-supplied names
314
+ # the project did not enable.
315
+ # - `dynamic.shape.lossy-projection` for every shape-projection
316
+ # type function (`pick_of`, …) applied to a carrier that
317
+ # loses precision (anything other than `HashShape` / `Tuple`).
318
+ #
319
+ # Both are authored `:info`; the severity profile re-stamps
320
+ # them per project taste. Path / line / column come from the
321
+ # annotation's `RBS::Location` when available, falling back
322
+ # to `.rigor.yml`-style file-level attribution otherwise.
323
+ def rbs_extended_reporter_diagnostics
324
+ return [] if @rbs_extended_reporter.empty?
325
+
326
+ unresolved = @rbs_extended_reporter.unresolved_payloads.map do |entry|
327
+ build_reporter_diagnostic(
328
+ entry.source_location,
329
+ rule: "dynamic.rbs-extended.unresolved",
330
+ message: "`RBS::Extended` directive payload could not be resolved: " \
331
+ "#{entry.payload.inspect}. Check for typos or enable a plugin " \
332
+ "that contributes the referenced type vocabulary."
333
+ )
334
+ end
335
+
336
+ lossy = @rbs_extended_reporter.lossy_projections.map do |entry|
337
+ build_reporter_diagnostic(
338
+ entry.source_location,
339
+ rule: "dynamic.shape.lossy-projection",
340
+ message: "Shape projection `#{entry.head}` applied to a carrier without a " \
341
+ "literal shape; the projection degrades to the input type. Author " \
342
+ "a `HashShape` / `Tuple` carrier or accept the unchanged result."
343
+ )
344
+ end
345
+
346
+ unresolved + lossy
347
+ end
348
+
349
+ # ADR-10 slice 5c — drains the per-run
350
+ # {DependencySourceInference::BoundaryCrossReporter} into
351
+ # `dynamic.dependency-source.boundary-cross` `:info`
352
+ # diagnostics. Each event flags a call site where RBS
353
+ # dispatch produced a concrete answer AND a `mode: :full`
354
+ # opt-in gem's source catalog ALSO contains an entry for
355
+ # the same `(class_name, method_name)` — i.e., both
356
+ # contracts have an opinion. RBS still wins on the
357
+ # dispatch result; the diagnostic is purely advisory so
358
+ # the user can verify the two contracts haven't drifted.
359
+ #
360
+ # Severity profile re-stamps the rule per project taste.
361
+ # The diagnostic carries no `path` / `line` / `column`
362
+ # because the crossing is per-method-per-gem, not
363
+ # per-call-site — the diagnostic anchors at `.rigor.yml`
364
+ # like the other `dependency-source.*` diagnostics that
365
+ # report on opt-in configuration.
366
+ def boundary_cross_diagnostics
367
+ return [] if @boundary_cross_reporter.empty?
368
+
369
+ @boundary_cross_reporter.entries.map do |entry|
370
+ Diagnostic.new(
371
+ path: ".rigor.yml", line: 1, column: 1,
372
+ message: "`#{entry.class_name}##{entry.method_name}` is contributed by both " \
373
+ "RBS (#{entry.rbs_display}) and the `mode: :full` opt-in gem " \
374
+ "`#{entry.gem_name}`. RBS wins on dispatch; verify the gem source " \
375
+ "has not drifted from its RBS contract.",
376
+ severity: :info,
377
+ rule: "dynamic.dependency-source.boundary-cross",
378
+ source_family: :builtin
379
+ )
380
+ end
381
+ end
382
+
383
+ def build_reporter_diagnostic(source_location, rule:, message:)
384
+ path, line, column = location_fields(source_location)
385
+ Diagnostic.new(
386
+ path: path, line: line, column: column,
387
+ message: message, severity: :info, rule: rule, source_family: :builtin
388
+ )
389
+ end
390
+
391
+ def location_fields(source_location)
392
+ return [".rigor.yml", 1, 1] if source_location.nil?
393
+
394
+ path = location_path(source_location)
395
+ line = source_location.respond_to?(:start_line) ? source_location.start_line : 1
396
+ column = source_location.respond_to?(:start_column) ? source_location.start_column + 1 : 1
397
+ [path, line, column]
398
+ rescue StandardError
399
+ [".rigor.yml", 1, 1]
400
+ end
401
+
402
+ def location_path(source_location)
403
+ buffer = source_location.respond_to?(:buffer) ? source_location.buffer : nil
404
+ return ".rigor.yml" if buffer.nil? || !buffer.respond_to?(:name)
405
+
406
+ name = buffer.name.to_s
407
+ name.empty? ? ".rigor.yml" : name
408
+ end
409
+
210
410
  # ADR-9 slice 3 — invokes every loaded plugin's `#prepare`
211
411
  # hook once per run, after the loader's `#init` pass and
212
412
  # before per-file iteration. Plugins publish facts here
@@ -364,7 +564,7 @@ module Rigor
364
564
  parse_result = Prism.parse_file(path, version: @configuration.target_ruby)
365
565
  return parse_diagnostics(path, parse_result) unless parse_result.errors.empty?
366
566
 
367
- scope = Scope.empty(environment: environment)
567
+ scope = Scope.empty(environment: environment, source_path: path)
368
568
  index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
369
569
  diagnostics = CheckRules.diagnose(
370
570
  path: path,