rigortype 0.1.0 → 0.1.2
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 +7 -2
- data/data/builtins/ruby_core/array.yml +6 -6
- data/data/builtins/ruby_core/hash.yml +1 -1
- data/data/builtins/ruby_core/io.yml +3 -3
- data/data/builtins/ruby_core/numeric.yml +1 -1
- data/data/builtins/ruby_core/pathname.yml +100 -100
- data/data/builtins/ruby_core/proc.yml +1 -1
- data/data/builtins/ruby_core/range.yml +6 -4
- data/data/builtins/ruby_core/string.yml +15 -10
- data/data/builtins/ruby_core/time.yml +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +116 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +123 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +118 -0
- data/lib/rigor/analysis/check_rules.rb +346 -18
- data/lib/rigor/analysis/rule_catalog.rb +343 -0
- data/lib/rigor/analysis/runner.rb +90 -6
- data/lib/rigor/builtins/regex_refinement.rb +104 -0
- data/lib/rigor/cli/diff_command.rb +169 -0
- data/lib/rigor/cli/explain_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +3 -3
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli.rb +29 -5
- data/lib/rigor/configuration/severity_profile.rb +18 -3
- data/lib/rigor/configuration.rb +186 -13
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/inference/expression_typer.rb +3 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +43 -2
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +104 -12
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/narrowing.rb +150 -6
- data/lib/rigor/inference/scope_indexer.rb +220 -17
- data/lib/rigor/inference/statement_evaluator.rb +29 -0
- data/lib/rigor/plugin/base.rb +43 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/io_boundary.rb +92 -19
- data/lib/rigor/plugin/load_error.rb +14 -2
- data/lib/rigor/plugin/loader.rb +116 -0
- data/lib/rigor/plugin/manifest.rb +75 -6
- data/lib/rigor/plugin/services.rb +14 -2
- data/lib/rigor/plugin/trust_policy.rb +30 -7
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/scope.rb +30 -5
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +8 -2
- metadata +9 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optionparser"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
class CLI
|
|
8
|
+
# Executes `rigor diff <baseline.json> [paths...]`. Compares
|
|
9
|
+
# the current `rigor check` diagnostics against a saved
|
|
10
|
+
# baseline JSON (the output of a previous `rigor check
|
|
11
|
+
# --format=json` run) and prints the delta:
|
|
12
|
+
#
|
|
13
|
+
# - **new** — diagnostics in the current run that were not
|
|
14
|
+
# in the baseline (typically a regression introduced in
|
|
15
|
+
# this PR).
|
|
16
|
+
# - **fixed** — diagnostics in the baseline that no longer
|
|
17
|
+
# appear in the current run (typically progress).
|
|
18
|
+
#
|
|
19
|
+
# Identity for matching is the tuple
|
|
20
|
+
# `(path, line, column, rule, source_family, message)`.
|
|
21
|
+
# An edit that moves a diagnostic to a new line surfaces as
|
|
22
|
+
# one fixed + one new pair, which lines up with the user's
|
|
23
|
+
# mental model of "you changed the line, the analyzer's
|
|
24
|
+
# position changed too."
|
|
25
|
+
#
|
|
26
|
+
# CI usage: commit a `rigor.baseline.json` produced once
|
|
27
|
+
# with `rigor check --format=json > rigor.baseline.json`,
|
|
28
|
+
# then run `rigor diff rigor.baseline.json` in CI. Exit code
|
|
29
|
+
# is `1` when any new diagnostic appears, `0` otherwise —
|
|
30
|
+
# so adding new errors fails CI but legacy errors recorded
|
|
31
|
+
# in the baseline don't.
|
|
32
|
+
class DiffCommand # rubocop:disable Metrics/ClassLength
|
|
33
|
+
USAGE = "Usage: rigor diff [options] <baseline.json> [paths...]"
|
|
34
|
+
|
|
35
|
+
def initialize(argv:, out:, err:)
|
|
36
|
+
@argv = argv
|
|
37
|
+
@out = out
|
|
38
|
+
@err = err
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Integer] CLI exit status.
|
|
42
|
+
def run
|
|
43
|
+
options = parse_options
|
|
44
|
+
|
|
45
|
+
baseline_path = @argv.shift
|
|
46
|
+
if baseline_path.nil?
|
|
47
|
+
@err.puts(USAGE)
|
|
48
|
+
return CLI::EXIT_USAGE
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
baseline = load_diagnostics(baseline_path)
|
|
52
|
+
current = options.fetch(:current_path) ? load_diagnostics(options.fetch(:current_path)) : run_current(options)
|
|
53
|
+
return CLI::EXIT_USAGE if baseline.nil? || current.nil?
|
|
54
|
+
|
|
55
|
+
diff = compute_diff(baseline, current)
|
|
56
|
+
write_diff(
|
|
57
|
+
diff, options.fetch(:format),
|
|
58
|
+
baseline_path: baseline_path,
|
|
59
|
+
baseline_count: baseline.size,
|
|
60
|
+
current_count: current.size
|
|
61
|
+
)
|
|
62
|
+
diff[:new].any? ? 1 : 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def parse_options
|
|
68
|
+
options = { format: "text", current_path: nil, config: nil }
|
|
69
|
+
OptionParser.new do |opt|
|
|
70
|
+
opt.banner = USAGE
|
|
71
|
+
opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
|
|
72
|
+
options[:format] = fmt
|
|
73
|
+
end
|
|
74
|
+
opt.on("--current=PATH", "Compare to the saved current JSON instead of running `rigor check`.") do |path|
|
|
75
|
+
options[:current_path] = path
|
|
76
|
+
end
|
|
77
|
+
opt.on("--config=PATH", "Path to .rigor.yml. Forwarded to the implicit `rigor check` run.") do |path|
|
|
78
|
+
options[:config] = path
|
|
79
|
+
end
|
|
80
|
+
end.parse!(@argv)
|
|
81
|
+
options
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Runs `rigor check` against the remaining argv (or the
|
|
85
|
+
# configured paths) and returns the diagnostics array.
|
|
86
|
+
# Reuses the analyzer + configuration plumbing the
|
|
87
|
+
# check-command path uses.
|
|
88
|
+
def run_current(options)
|
|
89
|
+
require_relative "../analysis/runner"
|
|
90
|
+
require_relative "../configuration"
|
|
91
|
+
|
|
92
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
93
|
+
paths = @argv.empty? ? configuration.paths : @argv
|
|
94
|
+
result = Analysis::Runner.new(configuration: configuration).run(paths)
|
|
95
|
+
result.diagnostics.map(&:to_h)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def load_diagnostics(path)
|
|
99
|
+
unless File.file?(path)
|
|
100
|
+
@err.puts("Baseline file not found: #{path}")
|
|
101
|
+
return nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
payload = JSON.parse(File.read(path))
|
|
105
|
+
payload.is_a?(Hash) ? Array(payload["diagnostics"]) : Array(payload)
|
|
106
|
+
rescue JSON::ParserError => e
|
|
107
|
+
@err.puts("Invalid JSON in #{path}: #{e.message}")
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
KEY_FIELDS = %w[path line column rule source_family message].freeze
|
|
112
|
+
private_constant :KEY_FIELDS
|
|
113
|
+
|
|
114
|
+
def compute_diff(baseline, current)
|
|
115
|
+
baseline_keys = baseline.to_set { |d| identity_for(d) }
|
|
116
|
+
current_keys = current.to_set { |d| identity_for(d) }
|
|
117
|
+
|
|
118
|
+
new_diags = current.reject { |d| baseline_keys.include?(identity_for(d)) }
|
|
119
|
+
fixed = baseline.reject { |d| current_keys.include?(identity_for(d)) }
|
|
120
|
+
{ new: new_diags, fixed: fixed }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def identity_for(diagnostic)
|
|
124
|
+
KEY_FIELDS.map { |k| diagnostic[k] }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def write_diff(diff, format, baseline_path:, baseline_count:, current_count:)
|
|
128
|
+
case format
|
|
129
|
+
when "json"
|
|
130
|
+
write_diff_json(diff, baseline_path, baseline_count, current_count)
|
|
131
|
+
else
|
|
132
|
+
write_diff_text(diff, baseline_path, baseline_count, current_count)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def write_diff_json(diff, baseline_path, baseline_count, current_count)
|
|
137
|
+
@out.puts(JSON.pretty_generate(
|
|
138
|
+
"baseline" => baseline_path,
|
|
139
|
+
"baseline_count" => baseline_count,
|
|
140
|
+
"current_count" => current_count,
|
|
141
|
+
"new" => diff[:new],
|
|
142
|
+
"fixed" => diff[:fixed]
|
|
143
|
+
))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def write_diff_text(diff, baseline_path, baseline_count, current_count)
|
|
147
|
+
@out.puts("# diff against #{baseline_path} (#{baseline_count} baseline / #{current_count} current)")
|
|
148
|
+
diff[:new].each { |d| @out.puts("+ NEW #{render_diagnostic(d)}") }
|
|
149
|
+
diff[:fixed].each { |d| @out.puts("- FIXED #{render_diagnostic(d)}") }
|
|
150
|
+
@out.puts("")
|
|
151
|
+
@out.puts("#{diff[:new].size} new, #{diff[:fixed].size} fixed")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def render_diagnostic(diagnostic)
|
|
155
|
+
rule = qualified_rule_for(diagnostic)
|
|
156
|
+
position = "#{diagnostic['path']}:#{diagnostic['line']}:#{diagnostic['column']}"
|
|
157
|
+
"#{position} [#{rule}] #{diagnostic['message']}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def qualified_rule_for(diagnostic)
|
|
161
|
+
family = diagnostic["source_family"]
|
|
162
|
+
rule = diagnostic["rule"]
|
|
163
|
+
return rule if family.nil? || family == "" || family == "builtin"
|
|
164
|
+
|
|
165
|
+
"#{family}.#{rule}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optionparser"
|
|
5
|
+
|
|
6
|
+
require_relative "../analysis/rule_catalog"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
class CLI
|
|
10
|
+
# Executes `rigor explain <rule>`. Prints the catalog entry for
|
|
11
|
+
# one canonical rule id, a legacy alias, or a family wildcard
|
|
12
|
+
# (`call`, `flow`, `assert`, `dump`, `def`).
|
|
13
|
+
#
|
|
14
|
+
# Without arguments lists every rule's id and one-line summary.
|
|
15
|
+
#
|
|
16
|
+
# The command is read-only: no parser, no analyzer, no I/O
|
|
17
|
+
# beyond the rendered catalog. Useful when a user sees a
|
|
18
|
+
# diagnostic in the editor and wants to know what the rule
|
|
19
|
+
# means without leaving the terminal.
|
|
20
|
+
class ExplainCommand
|
|
21
|
+
USAGE = "Usage: rigor explain [options] [<rule>]"
|
|
22
|
+
|
|
23
|
+
def initialize(argv:, out:, err:)
|
|
24
|
+
@argv = argv
|
|
25
|
+
@out = out
|
|
26
|
+
@err = err
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Integer] CLI exit status.
|
|
30
|
+
def run
|
|
31
|
+
options = parse_options
|
|
32
|
+
|
|
33
|
+
if @argv.empty?
|
|
34
|
+
render_index(options.fetch(:format))
|
|
35
|
+
return 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
token = @argv.shift
|
|
39
|
+
entries = Analysis::RuleCatalog.resolve(token)
|
|
40
|
+
if entries.empty?
|
|
41
|
+
@err.puts("Unknown rule: #{token}")
|
|
42
|
+
@err.puts("Run `rigor explain` with no arguments to list every rule.")
|
|
43
|
+
return CLI::EXIT_USAGE
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
render_entries(entries, options.fetch(:format))
|
|
47
|
+
0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def parse_options
|
|
53
|
+
options = { format: "text" }
|
|
54
|
+
OptionParser.new do |opt|
|
|
55
|
+
opt.banner = USAGE
|
|
56
|
+
opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
|
|
57
|
+
options[:format] = fmt
|
|
58
|
+
end
|
|
59
|
+
end.parse!(@argv)
|
|
60
|
+
options
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_index(format)
|
|
64
|
+
case format
|
|
65
|
+
when "json" then @out.puts(JSON.pretty_generate(Analysis::RuleCatalog.all.map(&:to_h)))
|
|
66
|
+
else render_index_text
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_index_text
|
|
71
|
+
@out.puts("Available rules:")
|
|
72
|
+
@out.puts("")
|
|
73
|
+
Analysis::RuleCatalog.all.each do |entry|
|
|
74
|
+
@out.puts(" #{entry.id.ljust(33)} #{entry.summary}")
|
|
75
|
+
end
|
|
76
|
+
@out.puts("")
|
|
77
|
+
@out.puts("Run `rigor explain <rule>` for the full description.")
|
|
78
|
+
@out.puts("Family wildcards (`call`, `flow`, `assert`, `dump`, `def`) print every rule under that prefix.")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_entries(entries, format)
|
|
82
|
+
case format
|
|
83
|
+
when "json" then @out.puts(JSON.pretty_generate(entries.map(&:to_h)))
|
|
84
|
+
else
|
|
85
|
+
entries.each_with_index do |entry, index|
|
|
86
|
+
@out.puts("") if index.positive?
|
|
87
|
+
render_entry_text(entry)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_entry_text(entry)
|
|
93
|
+
@out.puts(entry.id)
|
|
94
|
+
@out.puts("=" * entry.id.length)
|
|
95
|
+
@out.puts("")
|
|
96
|
+
@out.puts(entry.summary)
|
|
97
|
+
@out.puts("")
|
|
98
|
+
render_aliases(entry)
|
|
99
|
+
render_severity(entry)
|
|
100
|
+
render_section("Fires when:", entry.fires_when)
|
|
101
|
+
render_section("Does not fire when:", entry.does_not_fire_when)
|
|
102
|
+
@out.puts("Suppression: #{entry.suppression}")
|
|
103
|
+
@out.puts("Since: rigor #{entry.since}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_aliases(entry)
|
|
107
|
+
return if entry.aliases.empty?
|
|
108
|
+
|
|
109
|
+
@out.puts("Legacy aliases: #{entry.aliases.join(', ')}")
|
|
110
|
+
@out.puts("")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def render_severity(entry)
|
|
114
|
+
@out.puts("Authored severity: :#{entry.severity_authored}")
|
|
115
|
+
profile_table = entry.severity_by_profile.map { |profile, sev| "#{profile} → :#{sev}" }.join(", ")
|
|
116
|
+
@out.puts("Severity by profile: #{profile_table}")
|
|
117
|
+
@out.puts("")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_section(heading, items)
|
|
121
|
+
return if items.empty?
|
|
122
|
+
|
|
123
|
+
@out.puts(heading)
|
|
124
|
+
items.each { |item| @out.puts(" - #{item}") }
|
|
125
|
+
@out.puts("")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -48,7 +48,7 @@ module Rigor
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
50
|
def parse_options
|
|
51
|
-
options = { format: "text", trace: false, config:
|
|
51
|
+
options = { format: "text", trace: false, config: nil }
|
|
52
52
|
|
|
53
53
|
parser = OptionParser.new do |opts|
|
|
54
54
|
opts.banner = USAGE
|
|
@@ -65,8 +65,9 @@ module Rigor
|
|
|
65
65
|
file, line, column = target
|
|
66
66
|
return 1 unless file_exists?(file)
|
|
67
67
|
|
|
68
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
68
69
|
source = File.read(file)
|
|
69
|
-
parse_result = Prism.parse(source, filepath: file)
|
|
70
|
+
parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
|
|
70
71
|
return 1 if parse_errors?(parse_result, file)
|
|
71
72
|
|
|
72
73
|
node = locate_node(source: source, root: parse_result.value, file: file, line: line, column: column)
|
|
@@ -74,7 +75,6 @@ module Rigor
|
|
|
74
75
|
return 1 if node.nil?
|
|
75
76
|
|
|
76
77
|
tracer = options[:trace] ? Inference::FallbackTracer.new : nil
|
|
77
|
-
configuration = Configuration.load(options.fetch(:config))
|
|
78
78
|
base_scope = Scope.empty(environment: project_environment(file, configuration))
|
|
79
79
|
|
|
80
80
|
# Build a per-node scope index so locals bound earlier in the
|
|
@@ -46,7 +46,7 @@ module Rigor
|
|
|
46
46
|
|
|
47
47
|
def parse_options
|
|
48
48
|
options = { format: "text", limit: 10, show_recognized: false, threshold: nil,
|
|
49
|
-
config:
|
|
49
|
+
config: nil }
|
|
50
50
|
|
|
51
51
|
parser = OptionParser.new do |opts|
|
|
52
52
|
opts.banner = USAGE
|
|
@@ -93,7 +93,7 @@ module Rigor
|
|
|
93
93
|
scope = Scope.empty(environment: project_environment(configuration))
|
|
94
94
|
scanner = Inference::CoverageScanner.new(scope: scope)
|
|
95
95
|
accumulator = ScanAccumulator.new
|
|
96
|
-
paths.each { |path| scan_one(path, scanner, accumulator) }
|
|
96
|
+
paths.each { |path| scan_one(path, scanner, accumulator, configuration) }
|
|
97
97
|
accumulator.to_report(paths, options)
|
|
98
98
|
end
|
|
99
99
|
|
|
@@ -107,9 +107,9 @@ module Rigor
|
|
|
107
107
|
)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def scan_one(path, scanner, accumulator)
|
|
110
|
+
def scan_one(path, scanner, accumulator, configuration)
|
|
111
111
|
source = File.read(path)
|
|
112
|
-
parse_result = Prism.parse(source, filepath: path)
|
|
112
|
+
parse_result = Prism.parse(source, filepath: path, version: configuration.target_ruby)
|
|
113
113
|
if parse_result.errors.any?
|
|
114
114
|
accumulator.record_parse_error(path, parse_result.errors)
|
|
115
115
|
return
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -22,7 +22,9 @@ module Rigor
|
|
|
22
22
|
"check" => :run_check,
|
|
23
23
|
"init" => :run_init,
|
|
24
24
|
"type-of" => :run_type_of,
|
|
25
|
-
"type-scan" => :run_type_scan
|
|
25
|
+
"type-scan" => :run_type_scan,
|
|
26
|
+
"explain" => :run_explain,
|
|
27
|
+
"diff" => :run_diff
|
|
26
28
|
}.freeze
|
|
27
29
|
|
|
28
30
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -70,11 +72,11 @@ module Rigor
|
|
|
70
72
|
|
|
71
73
|
options = parse_check_options
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
76
|
+
cache_root = configuration.cache_path
|
|
74
77
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
75
78
|
cache_store = options.fetch(:no_cache) ? nil : Cache::Store.new(root: cache_root)
|
|
76
79
|
|
|
77
|
-
configuration = Configuration.load(options.fetch(:config))
|
|
78
80
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
79
81
|
runner = Analysis::Runner.new(
|
|
80
82
|
configuration: configuration,
|
|
@@ -90,7 +92,9 @@ module Rigor
|
|
|
90
92
|
|
|
91
93
|
def parse_check_options
|
|
92
94
|
options = {
|
|
93
|
-
|
|
95
|
+
# `nil` triggers `Configuration.discover` (`.rigor.yml` then
|
|
96
|
+
# `.rigor.dist.yml`); an explicit `--config=PATH` overrides.
|
|
97
|
+
config: nil,
|
|
94
98
|
format: "text",
|
|
95
99
|
explain: false,
|
|
96
100
|
cache_stats: false,
|
|
@@ -170,9 +174,14 @@ module Rigor
|
|
|
170
174
|
end
|
|
171
175
|
|
|
172
176
|
def run_init
|
|
177
|
+
# Default destination is `.rigor.dist.yml` — the
|
|
178
|
+
# project-default config that gets committed. Developers
|
|
179
|
+
# who want a personal override layer create `.rigor.yml`
|
|
180
|
+
# alongside it (auto-discovery prefers `.rigor.yml` when
|
|
181
|
+
# both are present; no implicit merge).
|
|
173
182
|
options = {
|
|
174
183
|
force: false,
|
|
175
|
-
path:
|
|
184
|
+
path: ".rigor.dist.yml"
|
|
176
185
|
}
|
|
177
186
|
|
|
178
187
|
parser = OptionParser.new do |opts|
|
|
@@ -200,6 +209,7 @@ module Rigor
|
|
|
200
209
|
# most likely to want to edit.
|
|
201
210
|
def init_template
|
|
202
211
|
<<~YAML
|
|
212
|
+
# yaml-language-server: $schema=https://github.com/zenwerk/rigor/raw/master/schemas/rigor-config.schema.json
|
|
203
213
|
# Rigor configuration. See docs/CURRENT_WORK.md for the
|
|
204
214
|
# full set of features the analyzer ships in this preview.
|
|
205
215
|
#
|
|
@@ -257,6 +267,18 @@ module Rigor
|
|
|
257
267
|
TypeScanCommand.new(argv: @argv, out: @out, err: @err).run
|
|
258
268
|
end
|
|
259
269
|
|
|
270
|
+
def run_explain
|
|
271
|
+
require_relative "cli/explain_command"
|
|
272
|
+
|
|
273
|
+
ExplainCommand.new(argv: @argv, out: @out, err: @err).run
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def run_diff
|
|
277
|
+
require_relative "cli/diff_command"
|
|
278
|
+
|
|
279
|
+
DiffCommand.new(argv: @argv, out: @out, err: @err).run
|
|
280
|
+
end
|
|
281
|
+
|
|
260
282
|
def write_result(result, format)
|
|
261
283
|
case format
|
|
262
284
|
when "json"
|
|
@@ -294,6 +316,8 @@ module Rigor
|
|
|
294
316
|
init Create a starter .rigor.yml
|
|
295
317
|
type-of Print the inferred type at FILE:LINE:COL
|
|
296
318
|
type-scan Report Scope#type_of coverage across PATHs
|
|
319
|
+
explain Print the description of one or all CheckRules
|
|
320
|
+
diff Compare current diagnostics to a saved baseline JSON
|
|
297
321
|
version Print the Rigor version
|
|
298
322
|
help Print this help
|
|
299
323
|
HELP
|
|
@@ -43,9 +43,14 @@ module Rigor
|
|
|
43
43
|
"call.argument-type-mismatch" => :warning,
|
|
44
44
|
"call.possible-nil-receiver" => :warning,
|
|
45
45
|
"flow.always-raises" => :warning,
|
|
46
|
+
"flow.unreachable-branch" => :info,
|
|
47
|
+
"flow.dead-assignment" => :info,
|
|
48
|
+
"flow.always-truthy-condition" => :info,
|
|
46
49
|
"assert.type-mismatch" => :error,
|
|
47
50
|
"dump.type" => :info,
|
|
48
|
-
"def.return-type-mismatch" => :warning
|
|
51
|
+
"def.return-type-mismatch" => :warning,
|
|
52
|
+
"def.method-visibility-mismatch" => :warning,
|
|
53
|
+
"def.ivar-write-mismatch" => :warning
|
|
49
54
|
}.freeze,
|
|
50
55
|
balanced: {
|
|
51
56
|
"call.undefined-method" => :error,
|
|
@@ -53,9 +58,14 @@ module Rigor
|
|
|
53
58
|
"call.argument-type-mismatch" => :error,
|
|
54
59
|
"call.possible-nil-receiver" => :error,
|
|
55
60
|
"flow.always-raises" => :error,
|
|
61
|
+
"flow.unreachable-branch" => :warning,
|
|
62
|
+
"flow.dead-assignment" => :warning,
|
|
63
|
+
"flow.always-truthy-condition" => :warning,
|
|
56
64
|
"assert.type-mismatch" => :error,
|
|
57
65
|
"dump.type" => :info,
|
|
58
|
-
"def.return-type-mismatch" => :warning
|
|
66
|
+
"def.return-type-mismatch" => :warning,
|
|
67
|
+
"def.method-visibility-mismatch" => :error,
|
|
68
|
+
"def.ivar-write-mismatch" => :warning
|
|
59
69
|
}.freeze,
|
|
60
70
|
strict: {
|
|
61
71
|
"call.undefined-method" => :error,
|
|
@@ -63,9 +73,14 @@ module Rigor
|
|
|
63
73
|
"call.argument-type-mismatch" => :error,
|
|
64
74
|
"call.possible-nil-receiver" => :error,
|
|
65
75
|
"flow.always-raises" => :error,
|
|
76
|
+
"flow.unreachable-branch" => :error,
|
|
77
|
+
"flow.dead-assignment" => :error,
|
|
78
|
+
"flow.always-truthy-condition" => :error,
|
|
66
79
|
"assert.type-mismatch" => :error,
|
|
67
80
|
"dump.type" => :error,
|
|
68
|
-
"def.return-type-mismatch" => :error
|
|
81
|
+
"def.return-type-mismatch" => :error,
|
|
82
|
+
"def.method-visibility-mismatch" => :error,
|
|
83
|
+
"def.ivar-write-mismatch" => :error
|
|
69
84
|
}.freeze
|
|
70
85
|
}.freeze
|
|
71
86
|
|