rigortype 0.1.17 → 0.1.19

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -222
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
  8. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
  9. data/lib/rigor/analysis/check_rules.rb +275 -44
  10. data/lib/rigor/analysis/diagnostic.rb +8 -0
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
  12. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  13. data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
  14. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  15. data/lib/rigor/analysis/runner.rb +207 -1200
  16. data/lib/rigor/analysis/worker_session.rb +60 -11
  17. data/lib/rigor/bleeding_edge.rb +123 -0
  18. data/lib/rigor/cache/descriptor.rb +86 -8
  19. data/lib/rigor/cache/incremental_snapshot.rb +10 -4
  20. data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
  21. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  22. data/lib/rigor/cache/store.rb +46 -13
  23. data/lib/rigor/cli/annotate_command.rb +100 -15
  24. data/lib/rigor/cli/check_command.rb +708 -0
  25. data/lib/rigor/cli/ci_detector.rb +94 -0
  26. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  27. data/lib/rigor/cli/plugins_command.rb +2 -4
  28. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  29. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  30. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  31. data/lib/rigor/cli/trace_command.rb +143 -0
  32. data/lib/rigor/cli/trace_renderer.rb +310 -0
  33. data/lib/rigor/cli/triage_command.rb +6 -3
  34. data/lib/rigor/cli/triage_renderer.rb +15 -1
  35. data/lib/rigor/cli.rb +21 -612
  36. data/lib/rigor/configuration/severity_profile.rb +13 -1
  37. data/lib/rigor/configuration.rb +66 -7
  38. data/lib/rigor/environment/rbs_loader.rb +78 -68
  39. data/lib/rigor/environment.rb +1 -1
  40. data/lib/rigor/inference/acceptance.rb +10 -0
  41. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  42. data/lib/rigor/inference/budget_trace.rb +29 -2
  43. data/lib/rigor/inference/expression_typer.rb +1080 -105
  44. data/lib/rigor/inference/flow_tracer.rb +180 -0
  45. data/lib/rigor/inference/macro_block_self_type.rb +11 -12
  46. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  47. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
  48. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  49. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  50. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  51. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
  52. data/lib/rigor/inference/method_dispatcher.rb +187 -55
  53. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  54. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  55. data/lib/rigor/inference/mutation_widening.rb +142 -0
  56. data/lib/rigor/inference/narrowing.rb +330 -37
  57. data/lib/rigor/inference/scope_indexer.rb +770 -39
  58. data/lib/rigor/inference/statement_evaluator.rb +998 -68
  59. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  60. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  61. data/lib/rigor/plugin/base.rb +517 -120
  62. data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
  63. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  64. data/lib/rigor/plugin/macro.rb +2 -3
  65. data/lib/rigor/plugin/manifest.rb +4 -24
  66. data/lib/rigor/plugin/node_rule_walk.rb +192 -0
  67. data/lib/rigor/plugin/registry.rb +264 -35
  68. data/lib/rigor/plugin.rb +1 -0
  69. data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
  70. data/lib/rigor/scope/discovery_index.rb +60 -0
  71. data/lib/rigor/scope.rb +199 -204
  72. data/lib/rigor/sig_gen/generator.rb +8 -0
  73. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  74. data/lib/rigor/source/literals.rb +14 -0
  75. data/lib/rigor/triage/catalogue.rb +4 -19
  76. data/lib/rigor/triage.rb +69 -1
  77. data/lib/rigor/type/combinator.rb +34 -0
  78. data/lib/rigor/version.rb +1 -1
  79. data/lib/rigor.rb +0 -1
  80. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
  81. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  82. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  83. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
  84. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  85. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  86. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
  87. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  88. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
  89. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  90. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  91. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
  92. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  93. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
  94. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  95. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
  96. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
  97. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  98. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  99. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  100. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
  101. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  102. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  103. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  104. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  105. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  106. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  107. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
  108. data/sig/rigor/analysis/fact_store.rbs +3 -0
  109. data/sig/rigor/environment.rbs +0 -2
  110. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  111. data/sig/rigor/inference.rbs +5 -0
  112. data/sig/rigor/plugin/base.rbs +6 -4
  113. data/sig/rigor/plugin/manifest.rbs +1 -2
  114. data/sig/rigor/scope.rbs +50 -29
  115. data/sig/rigor/source.rbs +1 -0
  116. data/sig/rigor/type.rbs +1 -0
  117. data/sig/rigor.rbs +1 -1
  118. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  119. data/skills/rigor-ci-setup/SKILL.md +319 -0
  120. data/skills/rigor-plugin-author/SKILL.md +6 -4
  121. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  122. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  123. metadata +21 -3
  124. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
  125. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "English"
3
4
  require "optionparser"
4
5
  require "prism"
5
6
 
@@ -8,6 +9,7 @@ require_relative "../environment"
8
9
  require_relative "../scope"
9
10
  require_relative "../inference/def_return_typer"
10
11
  require_relative "../inference/scope_indexer"
12
+ require_relative "../inference/statement_evaluator"
11
13
  require_relative "prism_colorizer"
12
14
  require_relative "command"
13
15
 
@@ -20,18 +22,33 @@ module Rigor
20
22
  # (so `1; 2; 3` reports `3`), or, for a line that no statement
21
23
  # closes, the widest expression ending there (so the `if nil`
22
24
  # header reports its condition). It infers that expression's
23
- # type and appends a `#=> dump_type: <type>` comment.
25
+ # type and appends a `#=> <type>` comment (the xmpfilter /
26
+ # seeing_is_believing convention).
24
27
  #
25
28
  # The annotated source is re-parsed with Prism — a sanity gate,
26
29
  # since the appended text is always a comment — and printed to
27
- # stdout with IRB-style syntax highlighting via
30
+ # stdout. When colour is enabled and `bat`
31
+ # (https://github.com/sharkdp/bat) is on PATH it is used for
32
+ # highlighting; otherwise IRB-style highlighting via
28
33
  # {PrismColorizer}.
29
34
  class AnnotateCommand < Command
30
35
  USAGE = "Usage: rigor annotate [options] FILE"
31
36
 
32
- # Appended ` #=> dump_type: <type>` suffix. Matched and
33
- # stripped before re-annotating so re-running is idempotent.
34
- ANNOTATION_PATTERN = /\s*#=>\s*dump_type:.*\z/
37
+ # Trailing `#=> …` annotation comment. Matched and stripped
38
+ # before re-annotating so re-running is idempotent — this
39
+ # follows xmpfilter's convention of owning the `#=>` marker,
40
+ # and also absorbs the pre-v0.2.0 `#=> dump_type: <type>`
41
+ # spelling. The leading `\s` requirement keeps a `#=>` inside
42
+ # a string literal (no preceding whitespace ambiguity aside)
43
+ # from matching mid-expression.
44
+ ANNOTATION_PATTERN = /\s+#=>(?:\s.*)?\z/
45
+
46
+ # Arguments for highlighting through `bat`: the annotated
47
+ # text arrives on stdin, so the language must be explicit;
48
+ # `--style=plain` drops the grid/header chrome so the output
49
+ # matches the PrismColorizer fallback line-for-line; paging
50
+ # stays off because the CLI may itself sit in a pipeline.
51
+ BAT_ARGS = %w[--language=ruby --style=plain --paging=never --color=always].freeze
35
52
 
36
53
  # @return [Integer] CLI exit status.
37
54
  def run
@@ -54,7 +71,7 @@ module Rigor
54
71
  def parse_options
55
72
  # Default: colour a tty, unless `NO_COLOR` opts out. An
56
73
  # explicit `--color` / `--no-color` overrides both.
57
- options = { config: nil, color: @out.tty? && !no_color_env? }
74
+ options = { config: nil, color: @out.tty? && !no_color_env?, bat: nil }
58
75
 
59
76
  parser = OptionParser.new do |opts|
60
77
  opts.banner = USAGE
@@ -63,6 +80,10 @@ module Rigor
63
80
  "Force or disable ANSI colour (default: auto-detect a tty; honours NO_COLOR)") do |value|
64
81
  options[:color] = value
65
82
  end
83
+ opts.on("--[no-]bat",
84
+ "Force or disable highlighting through bat (default: when colour is on and bat is found)") do |value|
85
+ options[:bat] = value
86
+ end
66
87
  end
67
88
  parser.parse!(@argv)
68
89
 
@@ -91,12 +112,17 @@ module Rigor
91
112
  parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
92
113
  return 1 if parse_errors?(parse_result, file)
93
114
 
115
+ # `converged_loop_recording` re-records fixpoint-tracked loop
116
+ # bodies from their converged (post-writeback) bindings, so a
117
+ # loop-body line annotates the joined widened type (`Integer`)
118
+ # rather than a stale first-iterations constant (`1 | 2`).
94
119
  scope_index = Inference::ScopeIndexer.index(
95
- parse_result.value, default_scope: base_scope(configuration)
120
+ parse_result.value, default_scope: base_scope(configuration),
121
+ converged_loop_recording: true
96
122
  )
97
123
  line_types = LineTypeCollector.new(scope_index).collect(parse_result.value)
98
124
 
99
- @out.puts(render(annotate(source, line_types), color: options.fetch(:color)))
125
+ @out.puts(render(annotate(source, line_types), color: options.fetch(:color), bat: options.fetch(:bat)))
100
126
  0
101
127
  end
102
128
 
@@ -118,8 +144,8 @@ module Rigor
118
144
  true
119
145
  end
120
146
 
121
- # Appends ` #=> dump_type: <type>` to every line a type was
122
- # inferred for, aligning the comment column.
147
+ # Appends ` #=> <type>` to every line a type was inferred
148
+ # for, aligning the comment column.
123
149
  def annotate(source, line_types)
124
150
  lines = source.lines
125
151
  column = annotation_column(lines, line_types)
@@ -130,7 +156,7 @@ module Rigor
130
156
  code = line.chomp.sub(ANNOTATION_PATTERN, "")
131
157
  next "#{code}#{eol}" if type.nil?
132
158
 
133
- "#{code.ljust(column)} #=> dump_type: #{type.describe(:short)}#{eol}"
159
+ "#{code.ljust(column)} #=> #{type.describe(:short)}#{eol}"
134
160
  end.join
135
161
  end
136
162
 
@@ -143,11 +169,46 @@ module Rigor
143
169
  widths.max || 0
144
170
  end
145
171
 
146
- def render(annotated, color:)
172
+ def render(annotated, color:, bat: nil)
147
173
  return annotated unless color
148
174
  return annotated unless Prism.parse(annotated).success?
149
175
 
150
- PrismColorizer.colorize(annotated)
176
+ rendered = render_with_bat(annotated, forced: bat) unless bat == false
177
+ rendered || PrismColorizer.colorize(annotated)
178
+ end
179
+
180
+ # Pipes the annotated source through `bat` and returns its
181
+ # highlighted output, or nil when bat is unavailable or
182
+ # fails (broken install, killed mid-write) — the caller
183
+ # falls back to {PrismColorizer}. An explicit `--bat` with
184
+ # no bat on PATH warns instead of failing silently.
185
+ def render_with_bat(annotated, forced: nil)
186
+ executable = bat_executable
187
+ if executable.nil?
188
+ @err.puts("annotate: --bat requested but no `bat` executable found on PATH") if forced
189
+ return nil
190
+ end
191
+
192
+ output = IO.popen([executable, *BAT_ARGS], "r+") do |io|
193
+ io.write(annotated)
194
+ io.close_write
195
+ io.read
196
+ end
197
+ return nil unless $CHILD_STATUS&.success?
198
+
199
+ output.empty? ? nil : output
200
+ rescue SystemCallError, IOError
201
+ nil
202
+ end
203
+
204
+ def bat_executable
205
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
206
+ next if dir.empty?
207
+
208
+ candidate = File.join(dir, "bat")
209
+ return candidate if File.file?(candidate) && File.executable?(candidate)
210
+ end
211
+ nil
151
212
  end
152
213
  end
153
214
 
@@ -203,11 +264,30 @@ module Rigor
203
264
  widest_per_line(program).each do |line, node|
204
265
  next if by_line.key?(line)
205
266
 
206
- type = type_of(node)
267
+ type = node.is_a?(Prism::BlockParametersNode) ? block_params_type(node) : type_of(node)
207
268
  by_line[line] = type unless type.nil?
208
269
  end
209
270
  end
210
271
 
272
+ # A `do |i|` header line's widest node is its BlockParametersNode —
273
+ # not an expression, so evaluating it would only echo the
274
+ # `Dynamic[top]` fallback. Annotate the line with the parameters'
275
+ # inferred bindings instead (the single param's type, or a tuple
276
+ # for multi-param blocks); decline (nil) when any param has no
277
+ # plain name or no recorded binding, leaving the line bare.
278
+ def block_params_type(params_node)
279
+ inner = params_node.parameters
280
+ return nil if inner.nil? || inner.requireds.empty?
281
+
282
+ scope = @scope_index[params_node]
283
+ types = inner.requireds.map do |param|
284
+ return nil unless param.respond_to?(:name)
285
+
286
+ scope.local(param.name) or return nil
287
+ end
288
+ types.size == 1 ? types.first : Type::Combinator.tuple_of(*types)
289
+ end
290
+
211
291
  def widest_per_line(program)
212
292
  widest = {}
213
293
  walk(program) do |node|
@@ -231,8 +311,13 @@ module Rigor
231
311
  node.compact_child_nodes.each { |child| walk(child, &block) }
232
312
  end
233
313
 
314
+ # Types the node through the flow evaluator (not the bare
315
+ # expression typer) under its recorded entry scope, so flow-only
316
+ # forms type as the engine sees them — `i += 1` dispatches `+` on
317
+ # `i`'s binding (`Integer`, post-fixpoint) instead of echoing the
318
+ # RHS literal's `1`.
234
319
  def type_of(node)
235
- @scope_index[node].type_of(node)
320
+ Inference::StatementEvaluator.new(scope: @scope_index[node]).evaluate(node).first
236
321
  rescue StandardError
237
322
  nil
238
323
  end