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.
- checksums.yaml +4 -4
- data/README.md +159 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +187 -55
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +192 -0
- data/lib/rigor/plugin/registry.rb +264 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- 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 `#=>
|
|
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
|
|
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
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
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 ` #=>
|
|
122
|
-
#
|
|
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)} #=>
|
|
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
|
-
|
|
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].
|
|
320
|
+
Inference::StatementEvaluator.new(scope: @scope_index[node]).evaluate(node).first
|
|
236
321
|
rescue StandardError
|
|
237
322
|
nil
|
|
238
323
|
end
|