rigortype 0.1.3 → 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 +125 -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 +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -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 +114 -3
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +1 -1
  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 +2 -2
  24. data/lib/rigor/configuration.rb +2 -2
  25. data/lib/rigor/environment.rb +35 -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 +3 -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 +146 -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 +7 -7
  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 +30 -9
  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 +5 -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 +52 -1
@@ -18,7 +18,7 @@ module Rigor
18
18
  relational
19
19
  ].freeze
20
20
 
21
- Target = Data.define(:kind, :name) do
21
+ class Target < Data.define(:kind, :name)
22
22
  def self.local(name)
23
23
  new(kind: :local, name: name.to_sym)
24
24
  end
@@ -28,7 +28,7 @@ module Rigor
28
28
  end
29
29
  end
30
30
 
31
- Fact = Data.define(:bucket, :target, :predicate, :payload, :polarity, :stability) do
31
+ class Fact < Data.define(:bucket, :target, :predicate, :payload, :polarity, :stability)
32
32
  def initialize(bucket:, target:, predicate:, payload: nil, polarity: :positive, stability: :local_binding)
33
33
  bucket = bucket.to_sym
34
34
  raise ArgumentError, "unknown fact bucket #{bucket.inspect}" unless BUCKETS.include?(bucket)
@@ -125,8 +125,16 @@ module Rigor
125
125
  unique.freeze
126
126
  end
127
127
 
128
+ # `fact.target` is `Target | Array[Target]` per the carrier
129
+ # contract. Branching with an early return on the `Array`
130
+ # arm lets type narrowing collapse the post-return value to
131
+ # the bare `Target` case, so the wrapped tuple is `[Target]`
132
+ # and the union of return paths is exactly `Array[Target]`.
128
133
  def fact_targets(fact)
129
- Array(fact.target)
134
+ target = fact.target
135
+ return target if target.is_a?(Array)
136
+
137
+ [target]
130
138
  end
131
139
  end
132
140
  end
@@ -31,8 +31,8 @@ module Rigor
31
31
  # from `Configuration::SeverityProfile::PROFILES`.
32
32
  # - `since` — first version the rule shipped in.
33
33
  module RuleCatalog # rubocop:disable Metrics/ModuleLength
34
- Entry = Data.define(:id, :summary, :fires_when, :does_not_fire_when,
35
- :suppression, :severity_authored, :severity_by_profile, :since) do
34
+ class Entry < Data.define(:id, :summary, :fires_when, :does_not_fire_when,
35
+ :suppression, :severity_authored, :severity_by_profile, :since)
36
36
  def aliases
37
37
  CheckRules::LEGACY_RULE_ALIASES.select { |_legacy, canonical| canonical == id }.keys
38
38
  end
@@ -6,6 +6,7 @@ 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"
@@ -22,7 +23,8 @@ module Rigor
22
23
  RUBY_GLOB = "**/*.rb"
23
24
  DEFAULT_CACHE_ROOT = ".rigor/cache"
24
25
 
25
- attr_reader :cache_store, :plugin_registry, :dependency_source_index
26
+ attr_reader :cache_store, :plugin_registry, :dependency_source_index,
27
+ :rbs_extended_reporter, :boundary_cross_reporter
26
28
 
27
29
  # @param configuration [Rigor::Configuration]
28
30
  # @param explain [Boolean] surface fail-soft fallback events
@@ -42,6 +44,8 @@ module Rigor
42
44
  @plugin_requirer = plugin_requirer
43
45
  @plugin_registry = Plugin::Registry::EMPTY
44
46
  @dependency_source_index = DependencySourceInference::Index::EMPTY
47
+ @rbs_extended_reporter = RbsExtended::Reporter.new
48
+ @boundary_cross_reporter = DependencySourceInference::BoundaryCrossReporter.new
45
49
  end
46
50
 
47
51
  # Walks every Ruby file under `paths`, parses it, builds a
@@ -66,12 +70,16 @@ module Rigor
66
70
  signature_paths: @configuration.signature_paths,
67
71
  cache_store: @cache_store,
68
72
  plugin_registry: @plugin_registry,
69
- dependency_source_index: @dependency_source_index
73
+ dependency_source_index: @dependency_source_index,
74
+ rbs_extended_reporter: @rbs_extended_reporter,
75
+ boundary_cross_reporter: @boundary_cross_reporter
70
76
  )
71
77
  expansion = expand_paths(paths)
72
78
 
73
79
  diagnostics = pre_file_diagnostics(expansion)
74
80
  diagnostics += expansion.fetch(:files).flat_map { |path| analyze_file(path, environment) }
81
+ diagnostics += rbs_extended_reporter_diagnostics
82
+ diagnostics += boundary_cross_diagnostics
75
83
 
76
84
  Result.new(diagnostics: apply_severity_profile(diagnostics))
77
85
  end
@@ -296,6 +304,109 @@ module Rigor
296
304
  end
297
305
  end
298
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
+
299
410
  # ADR-9 slice 3 — invokes every loaded plugin's `#prepare`
300
411
  # hook once per run, after the loader's `#init` pass and
301
412
  # before per-file iteration. Plugins publish facts here
@@ -453,7 +564,7 @@ module Rigor
453
564
  parse_result = Prism.parse_file(path, version: @configuration.target_ruby)
454
565
  return parse_diagnostics(path, parse_result) unless parse_result.errors.empty?
455
566
 
456
- scope = Scope.empty(environment: environment)
567
+ scope = Scope.empty(environment: environment, source_path: path)
457
568
  index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
458
569
  diagnostics = CheckRules.diagnose(
459
570
  path: path,