rigortype 0.2.9 → 0.3.0

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/handbook/03-narrowing.md +2 -1
  4. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  5. data/docs/handbook/06-classes.md +18 -9
  6. data/docs/handbook/08-understanding-errors.md +1 -0
  7. data/docs/handbook/11-sig-gen.md +11 -1
  8. data/docs/handbook/appendix-mypy.md +4 -3
  9. data/docs/install.md +14 -6
  10. data/docs/manual/01-installation.md +98 -4
  11. data/docs/manual/02-cli-reference.md +49 -12
  12. data/docs/manual/03-configuration.md +34 -2
  13. data/docs/manual/04-diagnostics.md +23 -0
  14. data/docs/manual/07-plugins.md +4 -4
  15. data/docs/manual/09-editor-integration.md +4 -4
  16. data/docs/manual/11-ci.md +51 -0
  17. data/docs/manual/12-caching.md +65 -0
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  20. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  21. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  23. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  24. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  28. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  29. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
  30. data/lib/rigor/analysis/check_rules.rb +557 -95
  31. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/incremental_session.rb +456 -51
  34. data/lib/rigor/analysis/path_expansion.rb +42 -0
  35. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  36. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  37. data/lib/rigor/analysis/run_cache_key.rb +68 -0
  38. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  43. data/lib/rigor/analysis/runner.rb +343 -68
  44. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  45. data/lib/rigor/analysis/worker_session.rb +30 -2
  46. data/lib/rigor/bleeding_edge.rb +37 -8
  47. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  48. data/lib/rigor/cache/descriptor.rb +88 -28
  49. data/lib/rigor/cache/file_digest.rb +158 -0
  50. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  51. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  52. data/lib/rigor/cache/store.rb +33 -3
  53. data/lib/rigor/ci_detector.rb +90 -0
  54. data/lib/rigor/cli/annotate_command.rb +15 -9
  55. data/lib/rigor/cli/baseline_command.rb +4 -1
  56. data/lib/rigor/cli/check_command.rb +212 -27
  57. data/lib/rigor/cli/coverage_command.rb +6 -1
  58. data/lib/rigor/cli/docs_command.rb +0 -30
  59. data/lib/rigor/cli/doctor_command.rb +163 -23
  60. data/lib/rigor/cli/lsp_command.rb +5 -0
  61. data/lib/rigor/cli/mcp_command.rb +5 -0
  62. data/lib/rigor/cli/plugins_command.rb +36 -6
  63. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  64. data/lib/rigor/cli/probe_environment.rb +85 -0
  65. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  66. data/lib/rigor/cli/skill_command.rb +1 -32
  67. data/lib/rigor/cli/trace_command.rb +5 -6
  68. data/lib/rigor/cli/type_of_command.rb +9 -8
  69. data/lib/rigor/cli/type_scan_command.rb +8 -8
  70. data/lib/rigor/cli.rb +15 -4
  71. data/lib/rigor/config_audit.rb +48 -2
  72. data/lib/rigor/configuration/severity_profile.rb +31 -0
  73. data/lib/rigor/configuration.rb +156 -8
  74. data/lib/rigor/environment/default_libraries.rb +35 -0
  75. data/lib/rigor/environment/rbs_loader.rb +164 -5
  76. data/lib/rigor/environment.rb +19 -27
  77. data/lib/rigor/inference/budget_trace.rb +77 -2
  78. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  79. data/lib/rigor/inference/def_handle.rb +23 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  81. data/lib/rigor/inference/def_return_typer.rb +2 -1
  82. data/lib/rigor/inference/expression_typer.rb +361 -113
  83. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  84. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  85. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  86. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  87. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
  88. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  89. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  90. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  91. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  92. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  93. data/lib/rigor/inference/mutation_widening.rb +42 -8
  94. data/lib/rigor/inference/narrowing.rb +127 -17
  95. data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  97. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  98. data/lib/rigor/inference/scope_indexer.rb +451 -36
  99. data/lib/rigor/inference/statement_evaluator.rb +64 -15
  100. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  101. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  102. data/lib/rigor/inference/void_origin.rb +25 -0
  103. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  104. data/lib/rigor/language_server/completion_provider.rb +2 -1
  105. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  106. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  107. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  108. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  109. data/lib/rigor/plugin/base.rb +9 -22
  110. data/lib/rigor/plugin/inflector.rb +12 -3
  111. data/lib/rigor/plugin/io_boundary.rb +5 -1
  112. data/lib/rigor/plugin/isolation.rb +81 -11
  113. data/lib/rigor/plugin/load_error.rb +10 -1
  114. data/lib/rigor/plugin/loader.rb +96 -14
  115. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  116. data/lib/rigor/plugin/registry.rb +32 -23
  117. data/lib/rigor/plugin.rb +26 -0
  118. data/lib/rigor/protection/mutator.rb +3 -2
  119. data/lib/rigor/reflection.rb +64 -0
  120. data/lib/rigor/runtime/jit.rb +128 -0
  121. data/lib/rigor/scope/discovery_index.rb +12 -2
  122. data/lib/rigor/scope.rb +122 -28
  123. data/lib/rigor/sig_gen/classification.rb +5 -1
  124. data/lib/rigor/sig_gen/generator.rb +38 -6
  125. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  126. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  127. data/lib/rigor/sig_gen/renderer.rb +10 -0
  128. data/lib/rigor/sig_gen/write_result.rb +9 -4
  129. data/lib/rigor/sig_gen/writer.rb +24 -2
  130. data/lib/rigor/source/node_children.rb +116 -0
  131. data/lib/rigor/source/node_locator.rb +3 -1
  132. data/lib/rigor/source/node_walker.rb +4 -2
  133. data/lib/rigor/source.rb +1 -0
  134. data/lib/rigor/type/difference.rb +28 -24
  135. data/lib/rigor/type/hash_shape.rb +34 -10
  136. data/lib/rigor/version.rb +1 -1
  137. data/lib/rigortype.rb +24 -0
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  139. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  141. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  142. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
  143. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  146. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  147. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  148. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  149. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  150. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  151. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  152. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  153. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  154. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
  155. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  156. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
  157. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  158. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  159. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  160. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  162. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  164. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  166. data/sig/prism_node_children.rbs +9 -0
  167. data/sig/rigor/inference.rbs +7 -0
  168. data/sig/rigor/plugin/base.rbs +2 -3
  169. data/sig/rigor/reflection.rbs +1 -0
  170. data/sig/rigor/scope.rbs +12 -1
  171. data/skills/rigor-ci-setup/SKILL.md +10 -0
  172. data/skills/rigor-editor-setup/SKILL.md +11 -0
  173. data/skills/rigor-next-steps/SKILL.md +23 -2
  174. data/skills/rigor-plugin-author/SKILL.md +3 -3
  175. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  176. data/skills/rigor-plugin-review/SKILL.md +2 -2
  177. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  178. data/skills/rigor-project-init/SKILL.md +29 -3
  179. data/skills/rigor-project-init/references/01-detect.md +1 -1
  180. data/skills/rigor-project-init/references/02-configure.md +39 -5
  181. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  182. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  183. metadata +42 -24
  184. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -42,21 +42,43 @@ module Rigor
42
42
  paths = @argv.empty? ? configuration.paths : @argv
43
43
 
44
44
  observations = collect_observations(configuration, options)
45
- candidates = SigGen::Generator.new(configuration: configuration, paths: paths,
46
- observations: observations,
47
- include_private: options.fetch(:include_private)).run
45
+ generator = SigGen::Generator.new(configuration: configuration, paths: paths,
46
+ observations: observations,
47
+ include_private: options.fetch(:include_private))
48
+ candidates = generator.run
48
49
  mode = options.fetch(:mode).to_sym
49
50
 
50
- if mode == :write
51
- dispatch_write(candidates, configuration, options)
52
- else
53
- dispatch_print_or_diff(candidates, mode, options)
54
- end
55
- 0
51
+ status = if mode == :write
52
+ dispatch_write(candidates, configuration, options)
53
+ else
54
+ dispatch_print_or_diff(candidates, mode, options)
55
+ 0
56
+ end
57
+ report_unrenderable(generator.unrenderable)
58
+ status
56
59
  end
57
60
 
58
61
  private
59
62
 
63
+ # A method whose rendered RBS does not parse is a Rigor rendering defect, not a fact about the user's
64
+ # code — the generator skipped it (so the rest of the signatures are still usable and still valid), but
65
+ # staying silent would leave the user with a quietly incomplete `sig/` and us with an unreported bug.
66
+ # Reported on stderr so it never contaminates `--print` output being piped into a file.
67
+ def report_unrenderable(unrenderable)
68
+ return if unrenderable.empty?
69
+
70
+ @err.puts(
71
+ "rigor sig-gen: skipped #{unrenderable.size} method(s) whose generated RBS does not parse. " \
72
+ "This is a bug in Rigor's RBS rendering, not in your code — please report it. " \
73
+ "The remaining signatures are unaffected."
74
+ )
75
+ unrenderable.each do |method|
76
+ @err.puts(" #{method.path}: #{method.class_name}##{method.method_name}")
77
+ @err.puts(" rendered: #{method.rbs}")
78
+ @err.puts(" #{method.error}")
79
+ end
80
+ end
81
+
60
82
  def dispatch_print_or_diff(candidates, mode, options)
61
83
  SigGen::Renderer.new(out: @out).render(
62
84
  candidates: candidates,
@@ -66,6 +88,7 @@ module Rigor
66
88
  )
67
89
  end
68
90
 
91
+ # @return [Integer] exit status — non-zero when a file the user asked to write could not be written.
69
92
  def dispatch_write(candidates, configuration, options)
70
93
  layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
71
94
  path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
@@ -74,6 +97,10 @@ module Rigor
74
97
  results = writer.write_all(candidates)
75
98
 
76
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
77
104
  end
78
105
 
79
106
  # Slice 3 — collect call-site argument observations when `--params=observed` is set. When `--observe=PATH` is not
@@ -37,9 +37,7 @@ module Rigor
37
37
  # the SKILL. Also spelled `describe`, and
38
38
  # surfaced top-level as `rigor describe`.
39
39
  #
40
- # The pre-v0.3.0 verb spellings `rigor skill list` / `print <name>` / `path <name>` still work but emit a stderr
41
- # deprecation notice; they are removed in v0.3.0 (see docs/ROADMAP.md § "Scheduled CLI deprecations"). `describe` is
42
- # a no-argument action, not a name-slot verb, so it stays first-class alongside `--describe`.
40
+ # `describe` is a no-argument action, not a name-slot verb, so it stays first-class alongside `--describe`.
43
41
  class SkillCommand < Command
44
42
  USAGE = <<~USAGE
45
43
  Usage: rigor skill [<name>] [--full <name>] [--path <name>] [--list] [--describe]
@@ -60,30 +58,14 @@ module Rigor
60
58
  rigor skill --full rigor-baseline-reduce
61
59
  rigor skill --path rigor-baseline-reduce
62
60
  rigor skill --describe (also: rigor describe)
63
-
64
- Deprecated (removed in v0.3.0) — use the forms above:
65
- rigor skill list -> rigor skill --list
66
- rigor skill print <name> -> rigor skill <name>
67
- rigor skill path <name> -> rigor skill --path <name>
68
61
  USAGE
69
62
 
70
63
  # The bundled skills live at `<gem_root>/skills/`. From `lib/rigor/cli/skill_command.rb` that is three directories
71
64
  # up.
72
65
  SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
73
66
 
74
- # The verb subcommands the flags superseded keep working with a stderr deprecation notice until this version drops
75
- # them. Each maps to the canonical advice printed and the flag it rewrites to.
76
- LEGACY_VERB_REMOVAL = "v0.3.0"
77
- LEGACY_VERBS = {
78
- "list" => { old: "list", advice: "--list", flag: "--list" },
79
- "print" => { old: "print <name>", advice: "<name>", flag: "--print" },
80
- "path" => { old: "path <name>", advice: "--path <name>", flag: "--path" }
81
- }.freeze
82
-
83
67
  # @return [Integer] CLI exit status.
84
68
  def run
85
- rewrite_legacy_verb!
86
-
87
69
  case @argv.first
88
70
  when nil
89
71
  run_list
@@ -112,19 +94,6 @@ module Rigor
112
94
 
113
95
  private
114
96
 
115
- # Translate a deprecated verb spelling into its flag form, warning once on stderr, so the dispatch above only
116
- # handles canonical forms.
117
- def rewrite_legacy_verb!
118
- spec = LEGACY_VERBS[@argv.first]
119
- return unless spec
120
-
121
- @err.puts(
122
- "rigor skill: `#{spec.fetch(:old)}` is deprecated and will be removed in " \
123
- "#{LEGACY_VERB_REMOVAL}; use `rigor skill #{spec.fetch(:advice)}` instead."
124
- )
125
- @argv[0] = spec.fetch(:flag)
126
- end
127
-
128
97
  def run_list
129
98
  skills = discover_skills
130
99
  if skills.empty?
@@ -12,6 +12,7 @@ require_relative "../inference/flow_tracer"
12
12
  require_relative "../inference/scope_indexer"
13
13
  require_relative "command"
14
14
  require_relative "trace_renderer"
15
+ require_relative "probe_environment"
15
16
 
16
17
  module Rigor
17
18
  class CLI
@@ -90,13 +91,11 @@ module Rigor
90
91
  0
91
92
  end
92
93
 
93
- # Mirrors the single-file path `rigor type-of` takes: a project-aware environment, an empty seed scope, one
94
- # statement-level evaluation of the whole programbut recorded under the FlowTracer.
94
+ # Mirrors the single-file path `rigor type-of` takes: the plugin-aware environment (so a trace replays the
95
+ # same dispatch `rigor check` would, including plugin-synthesized RBS see {ProbeEnvironment}), an empty
96
+ # seed scope, one statement-level evaluation of the whole program — but recorded under the FlowTracer.
95
97
  def record_events(root, file, configuration)
96
- environment = Environment.for_project(
97
- libraries: configuration.libraries,
98
- signature_paths: configuration.signature_paths
99
- )
98
+ environment = ProbeEnvironment.build(configuration: configuration, source_files: [file])
100
99
  scope = Scope.empty(environment: environment, source_path: file)
101
100
  Inference::FlowTracer.record { scope.evaluate(root) }
102
101
  end
@@ -13,6 +13,7 @@ require_relative "../inference/scope_indexer"
13
13
  require_relative "type_of_renderer"
14
14
  require_relative "command"
15
15
  require_relative "options"
16
+ require_relative "probe_environment"
16
17
 
17
18
  module Rigor
18
19
  class CLI
@@ -92,14 +93,14 @@ module Rigor
92
93
  0
93
94
  end
94
95
 
95
- # Builds a project-aware environment relative to the probed file. Project-RBS auto-detection roots at CWD today;
96
- # future work will walk parent directories to find the enclosing `Gemfile`/`*.gemspec` so probes against files
97
- # outside the current process's CWD still see the right `sig/` tree.
98
- def project_environment(_file, configuration)
99
- Environment.for_project(
100
- libraries: configuration.libraries,
101
- signature_paths: configuration.signature_paths
102
- )
96
+ # Builds the plugin-aware environment relative to the probed file, so the reported type matches what `rigor
97
+ # check` computes for the same position including types synthesized from inline RBS annotations by the
98
+ # ADR-93 auto-wired `rigor-rbs-inline` plugin (see {ProbeEnvironment} for the #162 misattribution this
99
+ # closes). The probed file is threaded as the synthesizer's `source_files:`. Project-RBS auto-detection
100
+ # roots at CWD today; future work will walk parent directories to find the enclosing `Gemfile`/`*.gemspec`
101
+ # so probes against files outside the current process's CWD still see the right `sig/` tree.
102
+ def project_environment(file, configuration)
103
+ ProbeEnvironment.build(configuration: configuration, source_files: [file])
103
104
  end
104
105
 
105
106
  def file_exists?(file)
@@ -11,6 +11,7 @@ require_relative "../scope"
11
11
  require_relative "type_scan_renderer"
12
12
  require_relative "type_scan_report"
13
13
  require_relative "command"
14
+ require_relative "probe_environment"
14
15
 
15
16
  module Rigor
16
17
  class CLI
@@ -69,20 +70,19 @@ module Rigor
69
70
 
70
71
  def scan_paths(paths, options)
71
72
  configuration = Configuration.load(options.fetch(:config))
72
- scope = Scope.empty(environment: project_environment(configuration))
73
+ scope = Scope.empty(environment: project_environment(configuration, paths))
73
74
  scanner = Inference::CoverageScanner.new(scope: scope)
74
75
  accumulator = ScanAccumulator.new
75
76
  paths.each { |path| scan_one(path, scanner, accumulator, configuration) }
76
77
  accumulator.to_report(paths, options)
77
78
  end
78
79
 
79
- # Builds a project-aware environment that auto-detects `<cwd>/sig` by default and honours the configuration's
80
- # `libraries:` / `signature_paths:` keys when present.
81
- def project_environment(configuration)
82
- Environment.for_project(
83
- libraries: configuration.libraries,
84
- signature_paths: configuration.signature_paths
85
- )
80
+ # Builds the plugin-aware environment that auto-detects `<cwd>/sig` by default and honours the
81
+ # configuration's `libraries:` / `signature_paths:` keys when present. The scanned `paths` are threaded as
82
+ # the plugin `source_rbs_synthesizer` inputs so coverage reflects the same synthesized RBS `rigor check`
83
+ # sees (see {ProbeEnvironment}).
84
+ def project_environment(configuration, source_files)
85
+ ProbeEnvironment.build(configuration: configuration, source_files: source_files)
86
86
  end
87
87
 
88
88
  def scan_one(path, scanner, accumulator, configuration)
data/lib/rigor/cli.rb CHANGED
@@ -11,7 +11,7 @@ require_relative "analysis/diagnostic"
11
11
  require_relative "analysis/result"
12
12
  require_relative "cli/options"
13
13
  require_relative "cli/diagnostic_formats"
14
- require_relative "cli/ci_detector"
14
+ require_relative "ci_detector"
15
15
 
16
16
  module Rigor
17
17
  # The CLI class is a dispatcher: each `run_*` method delegates to a command-specific class once the command grows
@@ -20,6 +20,16 @@ module Rigor
20
20
  class CLI # rubocop:disable Metrics/ClassLength
21
21
  EXIT_USAGE = 64
22
22
 
23
+ # The published location of `schemas/rigor-config.schema.json`, written into the `.rigor.yml` that
24
+ # `rigor init` generates so an editor validates the file as the user types.
25
+ #
26
+ # This MUST equal the schema's own `$id`, and it MUST resolve. Both are gated
27
+ # (`spec/rigor/config_schema_spec.rb`) because neither failure has a symptom: an unreachable schema
28
+ # is indistinguishable from a schema with no complaints, so a stale URL here silently disables
29
+ # validation for every project that ran `rigor init`. It pointed at a nonexistent org until
30
+ # 2026-07-17 and nothing noticed. See `docs/internal-spec/config.md`.
31
+ CONFIG_SCHEMA_URL = "https://github.com/rigortype/rigor/raw/master/schemas/rigor-config.schema.json"
32
+
23
33
  HANDLERS = {
24
34
  "check" => :run_check,
25
35
  "init" => :run_init,
@@ -138,9 +148,10 @@ module Rigor
138
148
  # most likely to want to edit.
139
149
  def init_template
140
150
  <<~YAML
141
- # yaml-language-server: $schema=https://github.com/zenwerk/rigor/raw/master/schemas/rigor-config.schema.json
142
- # Rigor configuration. See docs/CURRENT_WORK.md for the
143
- # full set of features the analyzer ships in this preview.
151
+ # yaml-language-server: $schema=#{CONFIG_SCHEMA_URL}
152
+ # Rigor configuration. The full key reference is
153
+ # https://github.com/rigortype/rigor/blob/master/docs/manual/03-configuration.md
154
+ # (or run `rigor docs configuration`).
144
155
  #
145
156
  # Keys you may want to edit:
146
157
  # - target_ruby: minimum Ruby version your project targets.
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "did_you_mean"
4
+
3
5
  require_relative "signature_path_audit"
4
- require_relative "analysis/check_rules"
6
+ # ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
7
+ # `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
8
+ # off the inference engine so the boot-slimming hit path stays engine-free.
9
+ require_relative "analysis/check_rules/rule_ids"
5
10
 
6
11
  module Rigor
7
12
  # Audits a loaded {Configuration} for the class of mistake where a configured value
@@ -36,12 +41,53 @@ module Rigor
36
41
  # paths resolve against (the CLI's CWD), used only by the explicit-path checks.
37
42
  # @return [Array<Warning>]
38
43
  def self.warnings(configuration, project_root: Dir.pwd)
39
- signature_path_warnings(configuration) +
44
+ unknown_key_warnings(configuration) +
45
+ signature_path_warnings(configuration) +
40
46
  library_warnings(configuration) +
41
47
  rule_token_warnings(configuration) +
42
48
  explicit_path_warnings(configuration, project_root)
43
49
  end
44
50
 
51
+ # Top-level keys the loader does not own. The archetypal case is a typo — `excludee:` for
52
+ # `exclude:` — which the loader drops in silence, so the exclusion never applies and the run
53
+ # reports errors from the very files the user meant to skip, with nothing anywhere saying the key
54
+ # did nothing. That is exactly this module's class of mistake, and it was the one whole-key
55
+ # instance of it going unwarned.
56
+ #
57
+ # A reserved namespace ({Configuration::RESERVED_NAMESPACES}) is unknown *on purpose* — another
58
+ # implementation reads it from the same file — so it is exempt. {Configuration#unknown_keys}
59
+ # already applies that exemption; warning here would push users to delete the key their other
60
+ # tool needs.
61
+ #
62
+ # **Top level only, and deliberately.** A nested check would have to name each group's known keys,
63
+ # and DEFAULTS cannot supply them: `DEFAULTS["dependencies"]` carries `source_inference` and
64
+ # `budget_per_gem` but not `budget_overrun_strategy`, which is real (`Configuration::Dependencies`
65
+ # reads it), documented, and schema-declared — so a DEFAULTS-keyed nested check would flag a
66
+ # working config. `severity_overrides:` is an open map of rule ids besides. Nested unknown keys are
67
+ # the schema tier's job: every nested object in `schemas/rigor-config.schema.json` is
68
+ # `additionalProperties: false`, and `config_schema_spec` keeps it complete
69
+ # ([ADR-99](../../docs/adr/99-config-schema-authority.md), `docs/internal-spec/config.md`).
70
+ def self.unknown_key_warnings(configuration)
71
+ configuration.unknown_keys.map do |key|
72
+ suggestion = suggest_config_key(key)
73
+ hint = suggestion ? " Did you mean `#{suggestion}`?" : ""
74
+ Warning.new(
75
+ kind: :unknown_key,
76
+ message: "`#{key}` is not a recognized configuration key; it has no effect.#{hint}",
77
+ fields: { "key" => key, "suggestion" => suggestion }
78
+ )
79
+ end
80
+ end
81
+
82
+ # Nearest known key, or nil. Uses `DidYouMean::SpellChecker` — the engine behind Ruby's own
83
+ # `NoMethodError` hints, and the one `Plugin::Base.suggest` wraps. Called directly rather than
84
+ # through that helper because `plugin/base.rb` pulls Prism and the diagnostic model in, and
85
+ # ADR-87 WD4 keeps this file (loaded by every check) off that path; `did_you_mean` is stdlib.
86
+ def self.suggest_config_key(key)
87
+ DidYouMean::SpellChecker.new(dictionary: Configuration::KNOWN_KEYS).correct(key.to_s).first
88
+ end
89
+ private_class_method :suggest_config_key
90
+
45
91
  # `signature_paths:` entries that resolve to nothing — delegated to {SignaturePathAudit},
46
92
  # which mirrors the loader's `directory?` + recursive `**/*.rbs` acceptance test.
47
93
  def self.signature_path_warnings(configuration)
@@ -37,11 +37,15 @@ module Rigor
37
37
  "call.wrong-arity" => :error,
38
38
  "call.argument-type-mismatch" => :warning,
39
39
  "call.possible-nil-receiver" => :warning,
40
+ "call.raise-non-exception" => :warning,
40
41
  "flow.always-raises" => :warning,
41
42
  "flow.unreachable-branch" => :info,
42
43
  "flow.dead-assignment" => :info,
43
44
  "flow.always-truthy-condition" => :info,
44
45
  "flow.unreachable-clause" => :info,
46
+ "flow.duplicate-hash-key" => :info,
47
+ "flow.return-in-ensure" => :info,
48
+ "flow.shadowed-rescue-clause" => :info,
45
49
  "assert.type-mismatch" => :error,
46
50
  "dump.type" => :info,
47
51
  "def.return-type-mismatch" => :warning,
@@ -50,6 +54,16 @@ module Rigor
50
54
  "def.override-return-widened" => :off,
51
55
  "def.override-param-narrowed" => :off,
52
56
  "def.ivar-write-mismatch" => :warning,
57
+ # ADR-8 companion (PHPStan IgnoreParseErrorRule-modelled): a broken suppression comment is
58
+ # equally bad in every profile — it silently fails to do what the author believes it does — so
59
+ # both rules stay :warning across all three profiles (including strict).
60
+ "suppression.unknown-rule" => :warning,
61
+ "suppression.empty" => :warning,
62
+ "suppression.unknown-marker" => :warning,
63
+ # ADR-100 — a new required diagnostic (ADR-50 WD1), so it is `:off` in every shipped profile and
64
+ # reaches a user only through the `use-of-void-value` bleeding-edge feature, which overrides this
65
+ # to `:warning`.
66
+ "static.value-use.void" => :off,
53
67
  # Opt-in author assertion: you only see it if you wrote a
54
68
  # `conforms-to` directive, so it stays a :warning even in
55
69
  # lenient — it is never unsolicited noise.
@@ -62,6 +76,7 @@ module Rigor
62
76
  "call.wrong-arity" => :error,
63
77
  "call.argument-type-mismatch" => :error,
64
78
  "call.possible-nil-receiver" => :error,
79
+ "call.raise-non-exception" => :error,
65
80
  "flow.always-raises" => :error,
66
81
  "flow.unreachable-branch" => :warning,
67
82
  "flow.dead-assignment" => :warning,
@@ -71,6 +86,9 @@ module Rigor
71
86
  # green; promote to :warning once Mastodon/GitLab/Redmine triage
72
87
  # to zero net false positives.
73
88
  "flow.unreachable-clause" => :info,
89
+ "flow.duplicate-hash-key" => :warning,
90
+ "flow.return-in-ensure" => :warning,
91
+ "flow.shadowed-rescue-clause" => :warning,
74
92
  "assert.type-mismatch" => :error,
75
93
  "dump.type" => :info,
76
94
  "def.return-type-mismatch" => :warning,
@@ -79,6 +97,10 @@ module Rigor
79
97
  "def.override-return-widened" => :warning,
80
98
  "def.override-param-narrowed" => :warning,
81
99
  "def.ivar-write-mismatch" => :warning,
100
+ "suppression.unknown-rule" => :warning,
101
+ "suppression.empty" => :warning,
102
+ "suppression.unknown-marker" => :warning,
103
+ "static.value-use.void" => :off,
82
104
  "rbs_extended.unsatisfied-conformance" => :warning
83
105
  }.freeze,
84
106
  strict: {
@@ -88,6 +110,7 @@ module Rigor
88
110
  "call.wrong-arity" => :error,
89
111
  "call.argument-type-mismatch" => :error,
90
112
  "call.possible-nil-receiver" => :error,
113
+ "call.raise-non-exception" => :error,
91
114
  "flow.always-raises" => :error,
92
115
  "flow.unreachable-branch" => :error,
93
116
  "flow.dead-assignment" => :error,
@@ -96,6 +119,9 @@ module Rigor
96
119
  # below its :error siblings) while it proves out — see the
97
120
  # balanced-profile note above.
98
121
  "flow.unreachable-clause" => :warning,
122
+ "flow.duplicate-hash-key" => :error,
123
+ "flow.return-in-ensure" => :error,
124
+ "flow.shadowed-rescue-clause" => :error,
99
125
  "assert.type-mismatch" => :error,
100
126
  "dump.type" => :error,
101
127
  "def.return-type-mismatch" => :error,
@@ -104,6 +130,11 @@ module Rigor
104
130
  "def.override-return-widened" => :error,
105
131
  "def.override-param-narrowed" => :error,
106
132
  "def.ivar-write-mismatch" => :error,
133
+ "suppression.unknown-rule" => :warning,
134
+ "suppression.empty" => :warning,
135
+ "suppression.unknown-marker" => :warning,
136
+ # `:off` even under strict: the gate is `bleeding_edge:`, not the profile (ADR-50 WD1 / ADR-100).
137
+ "static.value-use.void" => :off,
107
138
  "rbs_extended.unsatisfied-conformance" => :error
108
139
  }.freeze
109
140
  }.freeze