rigortype 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -10
  3. data/data/core_overlay/pathname.rbs +5 -0
  4. data/data/core_overlay/resolv.rbs +31 -0
  5. data/data/core_overlay/string_scanner.rbs +5 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
  8. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
  9. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
  10. data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
  11. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
  12. data/docs/manual/02-cli-reference.md +71 -11
  13. data/docs/manual/03-configuration.md +16 -1
  14. data/docs/manual/04-diagnostics.md +8 -0
  15. data/docs/manual/12-caching.md +17 -5
  16. data/docs/manual/15-type-protection-coverage.md +122 -1
  17. data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
  19. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
  20. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
  21. data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
  22. data/lib/rigor/analysis/check_rules.rb +107 -28
  23. data/lib/rigor/analysis/incremental_session.rb +156 -11
  24. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
  25. data/lib/rigor/analysis/rule_catalog.rb +5 -1
  26. data/lib/rigor/analysis/run_cache_key.rb +17 -1
  27. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
  28. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
  29. data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
  30. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
  31. data/lib/rigor/analysis/runner.rb +78 -7
  32. data/lib/rigor/analysis/severity_stamp.rb +2 -1
  33. data/lib/rigor/bleeding_edge.rb +132 -7
  34. data/lib/rigor/cache/descriptor.rb +6 -1
  35. data/lib/rigor/cache/engine_source.rb +162 -0
  36. data/lib/rigor/cache/incremental_snapshot.rb +87 -18
  37. data/lib/rigor/cli/check_command.rb +49 -28
  38. data/lib/rigor/cli/coverage_command.rb +15 -2
  39. data/lib/rigor/cli/coverage_mutation.rb +242 -11
  40. data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
  41. data/lib/rigor/cli/fused_protection_report.rb +12 -3
  42. data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
  43. data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
  44. data/lib/rigor/cli/mutation_protection_report.rb +26 -5
  45. data/lib/rigor/cli/protection_renderer.rb +13 -0
  46. data/lib/rigor/cli/protection_report.rb +11 -3
  47. data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
  48. data/lib/rigor/cli/sig_gen_command.rb +5 -4
  49. data/lib/rigor/configuration.rb +45 -6
  50. data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
  51. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  52. data/lib/rigor/environment/rbs_loader.rb +434 -75
  53. data/lib/rigor/environment.rb +28 -13
  54. data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
  55. data/lib/rigor/inference/expression_typer.rb +62 -4
  56. data/lib/rigor/inference/fork_map.rb +47 -4
  57. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
  58. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  59. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
  60. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +158 -12
  61. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
  62. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +234 -11
  63. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
  64. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
  65. data/lib/rigor/inference/method_dispatcher.rb +41 -10
  66. data/lib/rigor/inference/mutation_widening.rb +30 -25
  67. data/lib/rigor/inference/narrowing.rb +3 -1
  68. data/lib/rigor/inference/optimistic_origin.rb +123 -0
  69. data/lib/rigor/inference/protection_scanner.rb +18 -2
  70. data/lib/rigor/inference/receiver_alias.rb +57 -0
  71. data/lib/rigor/inference/scope_indexer.rb +197 -55
  72. data/lib/rigor/inference/singleton_object_constant.rb +63 -0
  73. data/lib/rigor/inference/statement_evaluator.rb +55 -4
  74. data/lib/rigor/language_server/buffer_table.rb +22 -0
  75. data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
  76. data/lib/rigor/language_server/project_context.rb +45 -0
  77. data/lib/rigor/language_server/publish_batcher.rb +82 -0
  78. data/lib/rigor/language_server/server.rb +20 -1
  79. data/lib/rigor/language_server.rb +1 -0
  80. data/lib/rigor/plugin/node_rule_walk.rb +6 -0
  81. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  82. data/lib/rigor/protection/closure_kill_oracle.rb +190 -0
  83. data/lib/rigor/protection/dependency_closure.rb +59 -0
  84. data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
  85. data/lib/rigor/protection/discovery_seed.rb +116 -0
  86. data/lib/rigor/protection/kill_signature.rb +31 -0
  87. data/lib/rigor/protection/mutation_cache.rb +355 -0
  88. data/lib/rigor/protection/mutation_scanner.rb +55 -11
  89. data/lib/rigor/protection/mutator.rb +26 -4
  90. data/lib/rigor/runtime/jit.rb +63 -1
  91. data/lib/rigor/scope/discovery_index.rb +9 -0
  92. data/lib/rigor/scope.rb +114 -10
  93. data/lib/rigor/sig_gen/generator.rb +130 -31
  94. data/lib/rigor/sig_gen/layout_index.rb +6 -0
  95. data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
  96. data/lib/rigor/sig_gen/renderer.rb +20 -1
  97. data/lib/rigor/sig_gen/write_result.rb +6 -4
  98. data/lib/rigor/sig_gen/writer.rb +12 -0
  99. data/lib/rigor/sig_gen.rb +1 -0
  100. data/lib/rigor/source/node_walker.rb +12 -0
  101. data/lib/rigor/type/anonymous_class_name.rb +40 -0
  102. data/lib/rigor/type/nominal.rb +5 -2
  103. data/lib/rigor/type/singleton.rb +4 -1
  104. data/lib/rigor/version.rb +1 -1
  105. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
  106. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
  107. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
  108. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
  109. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
  110. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
  111. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
  112. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
  113. data/sig/rigor/inference/void_origin.rbs +6 -3
  114. data/sig/rigor/scope.rbs +12 -0
  115. data/sig/rigor.rbs +4 -0
  116. metadata +21 -1
@@ -13,8 +13,9 @@ module Rigor
13
13
  #
14
14
  # Prints the bleeding-edge overlay — the Rigor-maintained set of the next major's queued changes
15
15
  # ({Rigor::BleedingEdge}) — as an explicit list, and reports which of them the project's `bleeding_edge:`
16
- # configuration adopts. The overlay is empty in this release, so the command currently reports an empty set; it
17
- # becomes the inspection surface ADR-50 describes once a feature is queued.
16
+ # configuration adopts. Each feature is rendered with its kind: a `severity` feature prints the rule severity
17
+ # diff it imposes, and a `behaviour` feature has no such diff to print, so its summary carries the whole
18
+ # explanation. Ids that have already graduated to default-on (ADR-50 § WD7) are listed after the overlay.
18
19
  #
19
20
  # Read-only: it loads `.rigor.yml` to resolve the active selection but runs no analysis.
20
21
  class ShowBleedingedgeCommand < Command
@@ -62,7 +63,8 @@ module Rigor
62
63
  "overlay" => BleedingEdge.features.map(&:to_h),
63
64
  "selector" => configuration.to_h.fetch("bleeding_edge"),
64
65
  "active" => BleedingEdge.active_features(selector).map(&:id),
65
- "unknown_selected" => BleedingEdge.unknown_selected_ids(selector)
66
+ "unknown_selected" => BleedingEdge.unknown_selected_ids(selector),
67
+ "graduated" => BleedingEdge::GRADUATED
66
68
  ))
67
69
  end
68
70
 
@@ -74,6 +76,7 @@ module Rigor
74
76
  else
75
77
  render_overlay
76
78
  end
79
+ render_graduated
77
80
  @out.puts("")
78
81
  render_selection(configuration)
79
82
  end
@@ -88,7 +91,7 @@ module Rigor
88
91
  @out.puts("#{BleedingEdge.features.length} feature(s) queued for the next major:")
89
92
  @out.puts("")
90
93
  BleedingEdge.features.each do |feature|
91
- @out.puts(" #{feature.id}")
94
+ @out.puts(" #{feature.id} [#{feature.kind}]")
92
95
  @out.puts(" #{feature.summary}")
93
96
  feature.severity_overrides.each do |rule, severity|
94
97
  @out.puts(" severity: #{rule} → :#{severity}")
@@ -96,6 +99,16 @@ module Rigor
96
99
  end
97
100
  end
98
101
 
102
+ # ADR-50 § WD7 — only printed when something has graduated, so a release with an empty list reads exactly
103
+ # as it did before graduation existed.
104
+ def render_graduated
105
+ return if BleedingEdge::GRADUATED.empty?
106
+
107
+ @out.puts("")
108
+ @out.puts("Graduated — on by default, no longer selectable:")
109
+ BleedingEdge::GRADUATED.each { |id| @out.puts(" #{id}") }
110
+ end
111
+
99
112
  def render_selection(configuration)
100
113
  selector = configuration.bleeding_edge
101
114
  active = BleedingEdge.active_features(selector).map(&:id)
@@ -97,10 +97,11 @@ module Rigor
97
97
  results = writer.write_all(candidates)
98
98
 
99
99
  SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format))
100
- # An assembled file that does not parse is refused rather than written. The user asked for a write and
101
- # did not get one, so the command must not report success — a green `sig-gen --write` in CI would
102
- # otherwise mean nothing.
103
- results.any? { |result| result.action == :skipped_invalid_rbs } ? 1 : 0
100
+ # A refused write (an assembled file that does not parse, or an existing target that is not valid
101
+ # UTF-8) means the user asked for a write and did not get one, so the command must not report
102
+ # success — a green `sig-gen --write` in CI would otherwise mean nothing.
103
+ refusals = %i[skipped_invalid_rbs skipped_invalid_encoding]
104
+ results.any? { |result| refusals.include?(result.action) } ? 1 : 0
104
105
  end
105
106
 
106
107
  # Slice 3 — collect call-site argument observations when `--params=observed` is set. When `--observe=PATH` is not
@@ -61,7 +61,9 @@ module Rigor
61
61
  # seed, so an undeclared `def` / `initialize` / setter parameter is typed to the union of its resolved
62
62
  # call-site argument types (precision-additive only — the in-body negative rules decline on the inferred
63
63
  # lower bound, WD6b). `false` (the default, resolved off by every severity profile) keeps the table empty
64
- # and the run byte-identical to today. Mutually exclusive with `--incremental` (WD6c).
64
+ # and the run byte-identical to today. Composes with `--incremental` (the WD6c mutual exclusion is
65
+ # lifted): the incremental session recomputes the table each run and diffs it against its snapshot,
66
+ # re-checking any callee whose seeds moved.
65
67
  "parameter_inference" => false,
66
68
  "cache" => {
67
69
  "path" => ".rigor/cache",
@@ -94,8 +96,9 @@ module Rigor
94
96
  # ADR-50 § WD2 — bleeding-edge overlay opt-in. Selects which of the *next major's* queued changes
95
97
  # ({Rigor::BleedingEdge}) this project adopts early. Orthogonal to `severity_profile:`. Accepts `false`
96
98
  # (default — adopt none), `true` (adopt the whole overlay), a list of feature ids (adopt only those),
97
- # or `{ all: true, except: [ids] }` (adopt all but the named). The overlay is empty today, so every
98
- # form is currently a no-op; it becomes live when the first discipline is queued for a major.
99
+ # or `{ all: true, except: [ids] }` (adopt all but the named). A queued change is either a severity
100
+ # promotion (composed by {SeverityProfile.resolve}) or a behaviour switch (read at its call site
101
+ # through {#bleeding_edge_active?}); `bleeding_edge:` selects both kinds the same way.
99
102
  "bleeding_edge" => false,
100
103
  "dependencies" => {
101
104
  "source_inference" => [],
@@ -437,6 +440,7 @@ module Rigor
437
440
  data.fetch("bleeding_edge", DEFAULTS.fetch("bleeding_edge"))
438
441
  )
439
442
  @bleeding_edge_severity_overrides = BleedingEdge.severity_overrides_for(@bleeding_edge)
443
+ @bleeding_edge_active_ids = BleedingEdge.active_ids_for(@bleeding_edge)
440
444
  @dependencies = Dependencies.from_h(
441
445
  data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
442
446
  )
@@ -515,6 +519,39 @@ module Rigor
515
519
  }
516
520
  end
517
521
 
522
+ # ADR-50 § WD2 — whether the bleeding-edge feature `id` is in force for this run.
523
+ #
524
+ # This is how a **behaviour** feature reaches its call site: a severity feature composes itself into
525
+ # `bleeding_edge_severity_overrides` and needs no engine code, but a queued change to a measurement, an
526
+ # algorithm, or a default has to be asked about where it happens. Answering from `Configuration` keeps
527
+ # the selection logic in one place and puts nothing but the id — already ADR-50 WD1 contract vocabulary
528
+ # — into engine code.
529
+ #
530
+ # Two deliberate asymmetries with the `bleeding_edge:` config key:
531
+ #
532
+ # - An id in {BleedingEdge::GRADUATED} answers `true` unconditionally. Graduation (§ WD7) turns a
533
+ # feature on for everyone, so a gate that outlives it keeps the graduated behaviour rather than
534
+ # silently reverting, and the call-site cleanup can land whenever it is convenient.
535
+ # - An id that is in neither {BleedingEdge::FEATURES} nor {BleedingEdge::GRADUATED} **raises**. An
536
+ # unknown id in a *config file* stays inert on purpose (it may come from a newer gem — see
537
+ # {#coerce_bleeding_edge}), but an id here is written by a Rigor contributor against the registry in
538
+ # the same checkout: a typo is a bug, and reading it as `false` would ship the feature permanently off
539
+ # with no signal.
540
+ #
541
+ # @param id [String] a feature id from {BleedingEdge::FEATURES} or {BleedingEdge::GRADUATED}.
542
+ # @return [Boolean]
543
+ # @raise [ArgumentError] if `id` names no known feature.
544
+ def bleeding_edge_active?(id)
545
+ return true if BleedingEdge.graduated?(id)
546
+ unless BleedingEdge.known_id?(id)
547
+ raise ArgumentError,
548
+ "unknown bleeding-edge feature id #{id.inspect}; known ids: " \
549
+ "#{(BleedingEdge.feature_ids + BleedingEdge::GRADUATED).inspect}"
550
+ end
551
+
552
+ @bleeding_edge_active_ids.include?(id)
553
+ end
554
+
518
555
  # ADR-50 § WD2 — returns a sibling Configuration whose bleeding-edge selection (and the derived
519
556
  # `bleeding_edge_severity_overrides` the two {SeverityProfile.resolve} sites consult) is replaced by
520
557
  # `value`, leaving every other field shared with the receiver. `value` takes the same forms as the
@@ -524,16 +561,18 @@ module Rigor
524
561
  #
525
562
  # The CLI's `--bleeding-edge[=ids]` / `--no-bleeding-edge` flag uses this to override the configured
526
563
  # selection for a single run (the same CLI-over-config precedence `--workers` and `--no-cache` follow).
527
- # It is a frozen `dup` with the two bleeding-edge ivars re-set: `dup` returns an unfrozen shallow copy
528
- # (every other ivar is the receiver's deeply-frozen value, safe to share read-only), the two replacements
564
+ # It is a frozen `dup` with the bleeding-edge ivars re-set: `dup` returns an unfrozen shallow copy
565
+ # (every other ivar is the receiver's deeply-frozen value, safe to share read-only), the replacements
529
566
  # are themselves deeply frozen, and the re-`freeze` keeps the result `Ractor.shareable?` for the worker
530
- # path.
567
+ # path. **Every ivar `#initialize` derives from `@bleeding_edge` has to be re-derived here** — the
568
+ # selector, the severity map, and the active-id set the {#bleeding_edge_active?} predicate reads.
531
569
  def with_bleeding_edge(value)
532
570
  selector = coerce_bleeding_edge(value)
533
571
  copy = dup
534
572
  copy.instance_variable_set(:@bleeding_edge, selector)
535
573
  copy.instance_variable_set(:@bleeding_edge_severity_overrides,
536
574
  BleedingEdge.severity_overrides_for(selector))
575
+ copy.instance_variable_set(:@bleeding_edge_active_ids, BleedingEdge.active_ids_for(selector))
537
576
  copy.freeze
538
577
  end
539
578
 
@@ -42,7 +42,8 @@ module Rigor
42
42
  "strscan",
43
43
  "prism", "rbs",
44
44
  # data/vendored_gem_sigs/
45
- "pg", "mysql2", "nokogiri", "bcrypt", "redis", "idn-ruby"
45
+ "pg", "mysql2", "nokogiri", "bcrypt", "redis", "idn-ruby", "racc",
46
+ "bundler", "rubygems"
46
47
  ].freeze
47
48
 
48
49
  # @param bundle_path [String, Pathname, nil] explicit path to the bundler install root. When `nil`,
@@ -32,7 +32,7 @@ module Rigor
32
32
  # hard-coding is acceptable; a directory walk at every call would add stat-cost to no benefit.)
33
33
  VENDORED_GEM_NAMES = Set[
34
34
  "ast", "bcrypt", "bundler", "cgi", "did_you_mean",
35
- "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "redis", "rubygems"
35
+ "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "racc", "redis", "rubygems"
36
36
  ].freeze
37
37
 
38
38
  # @param locked_gems [Hash{String => LockfileResolver::LockedGem}] The lockfile-resolved gem set. Empty