rigortype 0.1.16 → 0.1.18
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 +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
- data/lib/rigor/analysis/check_rules.rb +180 -73
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +18 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +477 -1110
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +50 -49
- data/lib/rigor/cache/incremental_snapshot.rb +153 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +145 -14
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +2 -7
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- 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 +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +15 -532
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +16 -3
- data/lib/rigor/environment/rbs_loader.rb +129 -71
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +149 -63
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +185 -84
- data/lib/rigor/inference/narrowing.rb +262 -5
- data/lib/rigor/inference/scope_indexer.rb +208 -21
- data/lib/rigor/inference/statement_evaluator.rb +110 -48
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +302 -45
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +281 -15
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +150 -167
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +22 -0
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +5 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- 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-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
- 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 +35 -18
- 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 +83 -36
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +27 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +42 -25
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +6 -1
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +36 -2
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# Runtime CI-environment detection (ADR-51 WD7), modelled on
|
|
6
|
+
# `OndraM/ci-detector` (the library PHPStan uses). Reads the well-known
|
|
7
|
+
# environment variables a CI provider sets and returns the matching
|
|
8
|
+
# {Platform}, classifying it into a **tier** that decides how
|
|
9
|
+
# `rigor check` surfaces diagnostics there:
|
|
10
|
+
#
|
|
11
|
+
# :native_stdout — Rigor has a native format that renders purely from
|
|
12
|
+
# stdout, so it is auto-emitted on top of the human
|
|
13
|
+
# output (GitHub Actions → `github`, TeamCity →
|
|
14
|
+
# `teamcity`). These are the first-class platforms.
|
|
15
|
+
# :native_artifact — Rigor has a native format but it needs a CI-wired
|
|
16
|
+
# report artifact, not stdout (GitLab CI → `gitlab`).
|
|
17
|
+
# First-class, but Rigor only *hints* the format.
|
|
18
|
+
# :reviewdog — no native Rigor format; second-class, routed through
|
|
19
|
+
# reviewdog (`checkstyle`/`sarif`) or `junit`. Hint
|
|
20
|
+
# only.
|
|
21
|
+
#
|
|
22
|
+
# Detection is a pure function of the environment hash, so it is fully
|
|
23
|
+
# testable; the CLI passes `ENV`. `RIGOR_CI_DETECT=0` (or `false`/`no`)
|
|
24
|
+
# disables it globally — the seam the spec suite uses for determinism.
|
|
25
|
+
module CiDetector
|
|
26
|
+
Platform = Struct.new(:id, :name, :format, :tier, keyword_init: true) do
|
|
27
|
+
def native_stdout? = tier == :native_stdout
|
|
28
|
+
def native_artifact? = tier == :native_artifact
|
|
29
|
+
def reviewdog? = tier == :reviewdog
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The detection table, ordered most-specific first so the generic
|
|
33
|
+
# `CI=true` catch-all is last (a provider that also sets `CI` is still
|
|
34
|
+
# recognised by its own variable). `match` is `:truthy` (value in
|
|
35
|
+
# 1/true/yes/on), `:present` (variable set non-empty), or `:equals`.
|
|
36
|
+
PROVIDERS = [
|
|
37
|
+
{ id: "github-actions", name: "GitHub Actions", format: "github", tier: :native_stdout,
|
|
38
|
+
var: "GITHUB_ACTIONS", match: :truthy },
|
|
39
|
+
{ id: "gitlab", name: "GitLab CI", format: "gitlab", tier: :native_artifact,
|
|
40
|
+
var: "GITLAB_CI", match: :truthy },
|
|
41
|
+
{ id: "teamcity", name: "TeamCity", format: "teamcity", tier: :native_stdout,
|
|
42
|
+
var: "TEAMCITY_VERSION", match: :present },
|
|
43
|
+
{ id: "circleci", name: "CircleCI", format: nil, tier: :reviewdog,
|
|
44
|
+
var: "CIRCLECI", match: :truthy },
|
|
45
|
+
{ id: "jenkins", name: "Jenkins", format: nil, tier: :reviewdog,
|
|
46
|
+
var: "JENKINS_URL", match: :present },
|
|
47
|
+
{ id: "travis", name: "Travis CI", format: nil, tier: :reviewdog,
|
|
48
|
+
var: "TRAVIS", match: :truthy },
|
|
49
|
+
{ id: "appveyor", name: "AppVeyor", format: nil, tier: :reviewdog,
|
|
50
|
+
var: "APPVEYOR", match: :truthy },
|
|
51
|
+
{ id: "azure-pipelines", name: "Azure Pipelines", format: nil, tier: :reviewdog,
|
|
52
|
+
var: "TF_BUILD", match: :present },
|
|
53
|
+
{ id: "bitbucket", name: "Bitbucket Pipelines", format: nil, tier: :reviewdog,
|
|
54
|
+
var: "BITBUCKET_BUILD_NUMBER", match: :present },
|
|
55
|
+
{ id: "buildkite", name: "Buildkite", format: nil, tier: :reviewdog,
|
|
56
|
+
var: "BUILDKITE", match: :truthy },
|
|
57
|
+
{ id: "drone", name: "Drone CI", format: nil, tier: :reviewdog,
|
|
58
|
+
var: "DRONE", match: :truthy },
|
|
59
|
+
{ id: "semaphore", name: "Semaphore", format: nil, tier: :reviewdog,
|
|
60
|
+
var: "SEMAPHORE", match: :truthy },
|
|
61
|
+
{ id: "codeship", name: "Codeship", format: nil, tier: :reviewdog,
|
|
62
|
+
var: "CI_NAME", match: :equals, value: "codeship" },
|
|
63
|
+
{ id: "ci", name: "CI", format: nil, tier: :reviewdog,
|
|
64
|
+
var: "CI", match: :truthy }
|
|
65
|
+
].freeze
|
|
66
|
+
|
|
67
|
+
module_function
|
|
68
|
+
|
|
69
|
+
# Returns the detected {Platform}, or nil when no CI is recognised or
|
|
70
|
+
# detection is disabled via `RIGOR_CI_DETECT`.
|
|
71
|
+
def detect(env = ENV)
|
|
72
|
+
return nil if disabled?(env)
|
|
73
|
+
|
|
74
|
+
row = PROVIDERS.find { |provider| matches?(env, provider) }
|
|
75
|
+
return nil if row.nil?
|
|
76
|
+
|
|
77
|
+
Platform.new(id: row[:id], name: row[:name], format: row[:format], tier: row[:tier])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def disabled?(env)
|
|
81
|
+
%w[0 false no off].include?(env["RIGOR_CI_DETECT"].to_s.strip.downcase)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def matches?(env, provider)
|
|
85
|
+
value = env[provider[:var]].to_s.strip
|
|
86
|
+
case provider[:match]
|
|
87
|
+
when :truthy then %w[1 true yes on].include?(value.downcase)
|
|
88
|
+
when :present then !value.empty?
|
|
89
|
+
when :equals then value.downcase == provider[:value]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# Base class for the `rigor <subcommand>` command objects.
|
|
6
|
+
#
|
|
7
|
+
# Every subcommand captured the same invariant wiring — the argument
|
|
8
|
+
# vector plus the output and error streams — in an identical
|
|
9
|
+
# `initialize(argv:, out:, err:)`, and defaulted the streams
|
|
10
|
+
# inconsistently (some to `$stdout` / `$stderr`, most not at all).
|
|
11
|
+
# Centralising it here gives one consistent shape and lets a test
|
|
12
|
+
# instantiate a command with just `argv:` (the streams default so a
|
|
13
|
+
# spec can pass a `StringIO` for one and ignore the other).
|
|
14
|
+
#
|
|
15
|
+
# Subclasses read the `@argv` / `@out` / `@err` ivars directly, as
|
|
16
|
+
# they did before.
|
|
17
|
+
class Command
|
|
18
|
+
def initialize(argv:, out: $stdout, err: $stderr)
|
|
19
|
+
@argv = argv
|
|
20
|
+
@out = out
|
|
21
|
+
@err = err
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Expands `args` (a mix of files and directories) into a unique
|
|
27
|
+
# list of `.rb` paths, recursing into directories. Returns nil —
|
|
28
|
+
# after writing `<command_name>: not a file or directory: <arg>` to
|
|
29
|
+
# `@err` — on the first arg that is neither. Shared by the
|
|
30
|
+
# path-walking commands (`type-scan`, `coverage`).
|
|
31
|
+
def collect_paths(args, command_name:)
|
|
32
|
+
paths = []
|
|
33
|
+
args.each do |arg|
|
|
34
|
+
if File.directory?(arg)
|
|
35
|
+
paths.concat(Dir.glob(File.join(arg, "**/*.rb")))
|
|
36
|
+
elsif File.file?(arg)
|
|
37
|
+
paths << arg
|
|
38
|
+
else
|
|
39
|
+
@err.puts("#{command_name}: not a file or directory: #{arg}")
|
|
40
|
+
return nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
paths.uniq
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -9,6 +9,7 @@ require_relative "../inference/precision_scanner"
|
|
|
9
9
|
require_relative "../scope"
|
|
10
10
|
require_relative "coverage_report"
|
|
11
11
|
require_relative "coverage_renderer"
|
|
12
|
+
require_relative "command"
|
|
12
13
|
|
|
13
14
|
module Rigor
|
|
14
15
|
class CLI
|
|
@@ -25,19 +26,13 @@ module Rigor
|
|
|
25
26
|
# 0 — scan complete, precision ratio ≥ threshold (or no threshold given)
|
|
26
27
|
# 1 — precision ratio < threshold, or parse errors encountered
|
|
27
28
|
# 64 — usage error
|
|
28
|
-
class CoverageCommand
|
|
29
|
+
class CoverageCommand < Command
|
|
29
30
|
USAGE = "Usage: rigor coverage [options] PATH..."
|
|
30
31
|
|
|
31
|
-
def initialize(argv:, out:, err:)
|
|
32
|
-
@argv = argv
|
|
33
|
-
@out = out
|
|
34
|
-
@err = err
|
|
35
|
-
end
|
|
36
|
-
|
|
37
32
|
# @return [Integer] CLI exit status.
|
|
38
33
|
def run
|
|
39
34
|
options = parse_options
|
|
40
|
-
paths = collect_paths(@argv)
|
|
35
|
+
paths = collect_paths(@argv, command_name: "coverage")
|
|
41
36
|
return CLI::EXIT_USAGE if paths.nil?
|
|
42
37
|
return usage_error if paths.empty?
|
|
43
38
|
|
|
@@ -64,21 +59,6 @@ module Rigor
|
|
|
64
59
|
options
|
|
65
60
|
end
|
|
66
61
|
|
|
67
|
-
def collect_paths(args)
|
|
68
|
-
paths = []
|
|
69
|
-
args.each do |arg|
|
|
70
|
-
if File.directory?(arg)
|
|
71
|
-
paths.concat(Dir.glob(File.join(arg, "**/*.rb")))
|
|
72
|
-
elsif File.file?(arg)
|
|
73
|
-
paths << arg
|
|
74
|
-
else
|
|
75
|
-
@err.puts("coverage: not a file or directory: #{arg}")
|
|
76
|
-
return nil
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
paths.uniq
|
|
80
|
-
end
|
|
81
|
-
|
|
82
62
|
def usage_error
|
|
83
63
|
@err.puts("coverage: at least one path is required")
|
|
84
64
|
@err.puts(USAGE)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "renderable"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
7
8
|
# Renders a `CoverageReport` as terminal-friendly text or JSON.
|
|
8
9
|
class CoverageRenderer
|
|
10
|
+
include Renderable
|
|
11
|
+
|
|
9
12
|
TIER_LABELS = {
|
|
10
13
|
constant: "constant",
|
|
11
14
|
nominal: "nominal",
|
|
@@ -21,14 +24,6 @@ module Rigor
|
|
|
21
24
|
@out = out
|
|
22
25
|
end
|
|
23
26
|
|
|
24
|
-
def render(report, format:)
|
|
25
|
-
case format
|
|
26
|
-
when "text" then render_text(report)
|
|
27
|
-
when "json" then render_json(report)
|
|
28
|
-
else raise OptionParser::InvalidArgument, "unsupported format: #{format}"
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
27
|
private
|
|
33
28
|
|
|
34
29
|
def render_text(report)
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "digest"
|
|
5
|
+
require_relative "../version"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
class CLI
|
|
9
|
+
# CI-native diagnostic output formats (ADR-51). Each renders an
|
|
10
|
+
# `Analysis::Result` to a string a CI platform consumes to surface
|
|
11
|
+
# diagnostics inline in a pull / merge request, rather than only in the
|
|
12
|
+
# job log. They read the same `Diagnostic` fields as `--format json`
|
|
13
|
+
# (path / line / column / severity / qualified rule / message) and add
|
|
14
|
+
# no new information — only a platform-native rendering of it.
|
|
15
|
+
#
|
|
16
|
+
# sarif — SARIF 2.1.0 (cross-platform; GitHub code-scanning, any
|
|
17
|
+
# other SARIF tool, and reviewdog `-f=sarif`)
|
|
18
|
+
# github — GitHub Actions workflow commands (`::error file=…,line=…::`)
|
|
19
|
+
# that the runner turns into inline PR annotations
|
|
20
|
+
# gitlab — GitLab Code Quality report JSON (the CodeClimate subset)
|
|
21
|
+
# that drives the merge-request Code Quality widget
|
|
22
|
+
# checkstyle — Checkstyle XML, the broad lint-interchange format that
|
|
23
|
+
# reviewdog (`-f=checkstyle`) and Jenkins/etc. consume
|
|
24
|
+
# junit — JUnit XML, the test-report format many CI systems render
|
|
25
|
+
#
|
|
26
|
+
# Severity maps once per format from Rigor's `:error` / `:warning` /
|
|
27
|
+
# `:info`; the qualified rule (`<source_family>.<rule>` or the bare rule
|
|
28
|
+
# for the builtin family) is the stable identifier, nil only for the
|
|
29
|
+
# ruleless producers (parse / internal errors), which each format
|
|
30
|
+
# degrades gracefully.
|
|
31
|
+
module DiagnosticFormats
|
|
32
|
+
FORMATS = %w[sarif github gitlab checkstyle junit teamcity].freeze
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def supports?(format)
|
|
37
|
+
FORMATS.include?(format)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Renders `result` in the named CI format. Callers gate on
|
|
41
|
+
# {.supports?} first; an unrecognised format returns nil.
|
|
42
|
+
def render(result, format)
|
|
43
|
+
case format
|
|
44
|
+
when "sarif" then Sarif.new(result).render
|
|
45
|
+
when "github" then GithubActions.new(result).render
|
|
46
|
+
when "gitlab" then GitlabCodeQuality.new(result).render
|
|
47
|
+
when "checkstyle" then Checkstyle.new(result).render
|
|
48
|
+
when "junit" then Junit.new(result).render
|
|
49
|
+
when "teamcity" then Teamcity.new(result).render
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# XML attribute / text escaping shared by the Checkstyle and JUnit
|
|
54
|
+
# formatters. Covers the five predefined XML entities so a diagnostic
|
|
55
|
+
# message carrying `<`, `&`, or a quote can't break the document.
|
|
56
|
+
module XmlEscaping
|
|
57
|
+
ENTITIES = { "&" => "&", "<" => "<", ">" => ">",
|
|
58
|
+
'"' => """, "'" => "'" }.freeze
|
|
59
|
+
|
|
60
|
+
def xml_escape(value)
|
|
61
|
+
value.to_s.gsub(/[&<>"']/, ENTITIES)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# SARIF 2.1.0 — the OASIS static-analysis interchange format. GitHub's
|
|
66
|
+
# `codeql-action/upload-sarif` ingests it to render findings on the PR
|
|
67
|
+
# diff and in the Security tab; it is the cross-platform anchor format.
|
|
68
|
+
class Sarif
|
|
69
|
+
SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json"
|
|
70
|
+
INFORMATION_URI = "https://github.com/rigortype/rigor"
|
|
71
|
+
|
|
72
|
+
# SARIF defines exactly three result levels; Rigor's `:info` is a
|
|
73
|
+
# `note` (the SARIF spelling for advisory findings).
|
|
74
|
+
LEVELS = { error: "error", warning: "warning", info: "note" }.freeze
|
|
75
|
+
|
|
76
|
+
def initialize(result)
|
|
77
|
+
@result = result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render
|
|
81
|
+
JSON.pretty_generate(document)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def document
|
|
87
|
+
{ "version" => "2.1.0", "$schema" => SCHEMA, "runs" => [run] }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run
|
|
91
|
+
{
|
|
92
|
+
"tool" => { "driver" => driver },
|
|
93
|
+
"results" => @result.diagnostics.map { |diagnostic| result_for(diagnostic) }
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def driver
|
|
98
|
+
{
|
|
99
|
+
"name" => "Rigor",
|
|
100
|
+
"informationUri" => INFORMATION_URI,
|
|
101
|
+
"version" => Rigor::VERSION,
|
|
102
|
+
"rules" => rules
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The distinct rule ids seen in this run, declared so consumers can
|
|
107
|
+
# cross-reference each result's `ruleId`. Id-only is a valid minimal
|
|
108
|
+
# SARIF rule object; richer per-rule metadata is a later enrichment.
|
|
109
|
+
def rules
|
|
110
|
+
@result.diagnostics.filter_map(&:qualified_rule).uniq.map { |id| { "id" => id } }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def result_for(diagnostic)
|
|
114
|
+
entry = {
|
|
115
|
+
"level" => LEVELS.fetch(diagnostic.severity, "warning"),
|
|
116
|
+
"message" => { "text" => diagnostic.message },
|
|
117
|
+
"locations" => [location_for(diagnostic)]
|
|
118
|
+
}
|
|
119
|
+
rule_id = diagnostic.qualified_rule
|
|
120
|
+
entry["ruleId"] = rule_id if rule_id
|
|
121
|
+
entry
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Rigor lines / columns are already 1-based, matching SARIF's
|
|
125
|
+
# 1-based `startLine` / `startColumn`. Paths are project-relative;
|
|
126
|
+
# SARIF URIs use forward slashes on every platform.
|
|
127
|
+
def location_for(diagnostic)
|
|
128
|
+
{
|
|
129
|
+
"physicalLocation" => {
|
|
130
|
+
"artifactLocation" => { "uri" => diagnostic.path.to_s.tr("\\", "/") },
|
|
131
|
+
"region" => { "startLine" => diagnostic.line, "startColumn" => diagnostic.column }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# GitHub Actions workflow commands — `::<level> file=…,line=…,col=…,
|
|
138
|
+
# title=…::<message>` lines the runner parses out of stdout and turns
|
|
139
|
+
# into inline PR annotations, with no separate upload step.
|
|
140
|
+
class GithubActions
|
|
141
|
+
# GitHub's annotation levels; Rigor's `:info` is a `notice`.
|
|
142
|
+
LEVELS = { error: "error", warning: "warning", info: "notice" }.freeze
|
|
143
|
+
|
|
144
|
+
def initialize(result)
|
|
145
|
+
@result = result
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def render
|
|
149
|
+
@result.diagnostics.map { |diagnostic| line_for(diagnostic) }.join("\n")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def line_for(diagnostic)
|
|
155
|
+
level = LEVELS.fetch(diagnostic.severity, "warning")
|
|
156
|
+
props = ["file=#{escape_property(diagnostic.path)}",
|
|
157
|
+
"line=#{diagnostic.line}", "col=#{diagnostic.column}"]
|
|
158
|
+
rule_id = diagnostic.qualified_rule
|
|
159
|
+
props << "title=#{escape_property(rule_id)}" if rule_id
|
|
160
|
+
"::#{level} #{props.join(',')}::#{escape_data(diagnostic.message)}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# GitHub's documented workflow-command escaping: `%` and the CR / LF
|
|
164
|
+
# that would otherwise terminate the command line, for message data.
|
|
165
|
+
def escape_data(value)
|
|
166
|
+
value.to_s.gsub("%", "%25").gsub("\r", "%0D").gsub("\n", "%0A")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Property values additionally escape the `,` (property separator)
|
|
170
|
+
# and `:` (command terminator) so a path or rule id can carry them.
|
|
171
|
+
def escape_property(value)
|
|
172
|
+
escape_data(value).gsub(",", "%2C").gsub(":", "%3A")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# GitLab Code Quality report — the CodeClimate-subset JSON array GitLab
|
|
177
|
+
# reads from a `codequality` CI artifact to populate the merge-request
|
|
178
|
+
# Code Quality widget.
|
|
179
|
+
class GitlabCodeQuality
|
|
180
|
+
# GitLab's severity vocabulary (a CodeClimate subset). Rigor maps
|
|
181
|
+
# error → major, warning → minor, info → info; `critical` / `blocker`
|
|
182
|
+
# are left for a louder future tier.
|
|
183
|
+
SEVERITIES = { error: "major", warning: "minor", info: "info" }.freeze
|
|
184
|
+
|
|
185
|
+
def initialize(result)
|
|
186
|
+
@result = result
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def render
|
|
190
|
+
JSON.pretty_generate(@result.diagnostics.map { |diagnostic| entry_for(diagnostic) })
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def entry_for(diagnostic)
|
|
196
|
+
{
|
|
197
|
+
"description" => description(diagnostic),
|
|
198
|
+
"check_name" => diagnostic.qualified_rule || "rigor",
|
|
199
|
+
"fingerprint" => fingerprint(diagnostic),
|
|
200
|
+
"severity" => SEVERITIES.fetch(diagnostic.severity, "minor"),
|
|
201
|
+
"location" => {
|
|
202
|
+
"path" => diagnostic.path.to_s,
|
|
203
|
+
"lines" => { "begin" => diagnostic.line }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The rule id is folded into the description (the widget shows it)
|
|
209
|
+
# because Code Quality has no dedicated rule field.
|
|
210
|
+
def description(diagnostic)
|
|
211
|
+
rule_id = diagnostic.qualified_rule
|
|
212
|
+
rule_id ? "#{diagnostic.message} [#{rule_id}]" : diagnostic.message
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# GitLab dedups findings by fingerprint and tracks them across runs
|
|
216
|
+
# by it, so it must be stable for an unchanged finding and unique per
|
|
217
|
+
# finding. Hashing the locating tuple (path, rule, line, column,
|
|
218
|
+
# message) satisfies both — order-independent, no run-volatile input.
|
|
219
|
+
def fingerprint(diagnostic)
|
|
220
|
+
payload = [diagnostic.path, diagnostic.qualified_rule, diagnostic.line,
|
|
221
|
+
diagnostic.column, diagnostic.message].join("")
|
|
222
|
+
Digest::SHA256.hexdigest(payload)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Checkstyle XML — the lint-interchange format a wide range of tools
|
|
227
|
+
# read, most usefully reviewdog (`-f=checkstyle`), which then posts to
|
|
228
|
+
# any of its reporters (GitHub PR review, GitLab MR, Gerrit, …). Errors
|
|
229
|
+
# are grouped by file; the qualified rule rides in `source` (the rule
|
|
230
|
+
# code reviewdog surfaces). Checkstyle's native severities are
|
|
231
|
+
# `error` / `warning` / `info`, so Rigor's map through unchanged.
|
|
232
|
+
class Checkstyle
|
|
233
|
+
include XmlEscaping
|
|
234
|
+
|
|
235
|
+
def initialize(result)
|
|
236
|
+
@result = result
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def render
|
|
240
|
+
lines = ['<?xml version="1.0" encoding="UTF-8"?>', "<checkstyle>"]
|
|
241
|
+
@result.diagnostics.group_by(&:path).each do |path, diagnostics|
|
|
242
|
+
lines << %( <file name="#{xml_escape(path)}">)
|
|
243
|
+
diagnostics.each { |diagnostic| lines << error_element(diagnostic) }
|
|
244
|
+
lines << " </file>"
|
|
245
|
+
end
|
|
246
|
+
lines << "</checkstyle>"
|
|
247
|
+
lines.join("\n")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
def error_element(diagnostic)
|
|
253
|
+
attrs = %(line="#{diagnostic.line}" column="#{diagnostic.column}" ) +
|
|
254
|
+
%(severity="#{diagnostic.severity}" message="#{xml_escape(diagnostic.message)}")
|
|
255
|
+
rule_id = diagnostic.qualified_rule
|
|
256
|
+
attrs += %( source="#{xml_escape(rule_id)}") if rule_id
|
|
257
|
+
" <error #{attrs} />"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# JUnit XML — the test-report format GitHub's test reporting, GitLab,
|
|
262
|
+
# Jenkins, and CircleCI render. Following the established linter
|
|
263
|
+
# convention (rubocop / eslint / PHPStan): every diagnostic is a
|
|
264
|
+
# `testcase` carrying a `failure` typed by its severity, so all of them
|
|
265
|
+
# are visible in the report. The exit code (errors only) remains the
|
|
266
|
+
# gate; this view is for surfacing, not gating.
|
|
267
|
+
class Junit
|
|
268
|
+
include XmlEscaping
|
|
269
|
+
|
|
270
|
+
def initialize(result)
|
|
271
|
+
@result = result
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def render
|
|
275
|
+
diagnostics = @result.diagnostics
|
|
276
|
+
# JUnit wants at least one test; a clean run reports one passing case.
|
|
277
|
+
tests = diagnostics.empty? ? 1 : diagnostics.size
|
|
278
|
+
lines = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
279
|
+
%(<testsuite name="rigor" tests="#{tests}" failures="#{diagnostics.size}">)]
|
|
280
|
+
if diagnostics.empty?
|
|
281
|
+
lines << ' <testcase name="rigor" />'
|
|
282
|
+
else
|
|
283
|
+
diagnostics.each { |diagnostic| lines.concat(testcase(diagnostic)) }
|
|
284
|
+
end
|
|
285
|
+
lines << "</testsuite>"
|
|
286
|
+
lines.join("\n")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
def testcase(diagnostic)
|
|
292
|
+
name = "#{diagnostic.path}:#{diagnostic.line}:#{diagnostic.column}"
|
|
293
|
+
classname = diagnostic.qualified_rule || "rigor"
|
|
294
|
+
[
|
|
295
|
+
%( <testcase name="#{xml_escape(name)}" classname="#{xml_escape(classname)}">),
|
|
296
|
+
%( <failure type="#{diagnostic.severity}" message="#{xml_escape(diagnostic.message)}" />),
|
|
297
|
+
" </testcase>"
|
|
298
|
+
]
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# TeamCity inspection service messages — the `##teamcity[…]` lines a
|
|
303
|
+
# TeamCity build agent parses out of the build log into its Inspections
|
|
304
|
+
# view. The one stdout-native format (besides `github`) that
|
|
305
|
+
# CI-detection auto-emits. One `inspectionType` declares the category;
|
|
306
|
+
# each diagnostic is an `inspection` typed by severity.
|
|
307
|
+
class Teamcity
|
|
308
|
+
SEVERITIES = { error: "ERROR", warning: "WARNING", info: "INFO" }.freeze
|
|
309
|
+
|
|
310
|
+
def initialize(result)
|
|
311
|
+
@result = result
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def render
|
|
315
|
+
return "" if @result.diagnostics.empty?
|
|
316
|
+
|
|
317
|
+
lines = [message("inspectionType", id: "rigor", name: "rigor",
|
|
318
|
+
category: "rigor", description: "Rigor inspection")]
|
|
319
|
+
@result.diagnostics.each { |diagnostic| lines << inspection(diagnostic) }
|
|
320
|
+
lines.join("\n")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
private
|
|
324
|
+
|
|
325
|
+
def inspection(diagnostic)
|
|
326
|
+
rule_id = diagnostic.qualified_rule
|
|
327
|
+
text = rule_id ? "#{diagnostic.message} [#{rule_id}]" : diagnostic.message
|
|
328
|
+
message("inspection", typeId: "rigor", message: text, file: diagnostic.path,
|
|
329
|
+
line: diagnostic.line, SEVERITY: SEVERITIES.fetch(diagnostic.severity, "WARNING"))
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def message(name, attrs)
|
|
333
|
+
pairs = attrs.map { |key, value| "#{key}='#{escape(value)}'" }.join(" ")
|
|
334
|
+
"##teamcity[#{name} #{pairs}]"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# TeamCity's documented service-message escaping.
|
|
338
|
+
def escape(value)
|
|
339
|
+
value.to_s.gsub("|", "||").gsub("'", "|'").gsub("\n", "|n")
|
|
340
|
+
.gsub("\r", "|r").gsub("[", "|[").gsub("]", "|]")
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "command"
|
|
4
|
+
|
|
3
5
|
require "json"
|
|
4
6
|
require "optionparser"
|
|
5
7
|
|
|
@@ -29,15 +31,9 @@ module Rigor
|
|
|
29
31
|
# is `1` when any new diagnostic appears, `0` otherwise —
|
|
30
32
|
# so adding new errors fails CI but legacy errors recorded
|
|
31
33
|
# in the baseline don't.
|
|
32
|
-
class DiffCommand
|
|
34
|
+
class DiffCommand < Command
|
|
33
35
|
USAGE = "Usage: rigor diff [options] <baseline.json> [paths...]"
|
|
34
36
|
|
|
35
|
-
def initialize(argv:, out:, err:)
|
|
36
|
-
@argv = argv
|
|
37
|
-
@out = out
|
|
38
|
-
@err = err
|
|
39
|
-
end
|
|
40
|
-
|
|
41
37
|
# @return [Integer] CLI exit status.
|
|
42
38
|
def run
|
|
43
39
|
options = parse_options
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "optionparser"
|
|
5
5
|
|
|
6
6
|
require_relative "../analysis/rule_catalog"
|
|
7
|
+
require_relative "command"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
class CLI
|
|
@@ -17,15 +18,9 @@ module Rigor
|
|
|
17
18
|
# beyond the rendered catalog. Useful when a user sees a
|
|
18
19
|
# diagnostic in the editor and wants to know what the rule
|
|
19
20
|
# means without leaving the terminal.
|
|
20
|
-
class ExplainCommand
|
|
21
|
+
class ExplainCommand < Command
|
|
21
22
|
USAGE = "Usage: rigor explain [options] [<rule>]"
|
|
22
23
|
|
|
23
|
-
def initialize(argv:, out:, err:)
|
|
24
|
-
@argv = argv
|
|
25
|
-
@out = out
|
|
26
|
-
@err = err
|
|
27
|
-
end
|
|
28
|
-
|
|
29
24
|
# @return [Integer] CLI exit status.
|
|
30
25
|
def run
|
|
31
26
|
options = parse_options
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "command"
|
|
4
|
+
|
|
3
5
|
require "optionparser"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -11,15 +13,9 @@ module Rigor
|
|
|
11
13
|
# The actual stdio JSON-RPC reader / writer is queued for slice 2;
|
|
12
14
|
# invoking `rigor lsp` at slice 1 returns immediately after
|
|
13
15
|
# validating the transport flag.
|
|
14
|
-
class LspCommand
|
|
16
|
+
class LspCommand < Command
|
|
15
17
|
USAGE = "Usage: rigor lsp [options]"
|
|
16
18
|
|
|
17
|
-
def initialize(argv:, out:, err:)
|
|
18
|
-
@argv = argv
|
|
19
|
-
@out = out
|
|
20
|
-
@err = err
|
|
21
|
-
end
|
|
22
|
-
|
|
23
19
|
# @return [Integer] CLI exit status.
|
|
24
20
|
def run
|
|
25
21
|
options = parse_options
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "command"
|
|
4
|
+
|
|
3
5
|
require "optionparser"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -13,15 +15,9 @@ module Rigor
|
|
|
13
15
|
# Slice 1 ships the stdio transport with seven read-only tools:
|
|
14
16
|
# rigor_check, rigor_type_of, rigor_triage, rigor_annotate,
|
|
15
17
|
# rigor_sig_gen, rigor_explain, rigor_coverage.
|
|
16
|
-
class McpCommand
|
|
18
|
+
class McpCommand < Command
|
|
17
19
|
USAGE = "Usage: rigor mcp [options]"
|
|
18
20
|
|
|
19
|
-
def initialize(argv:, out:, err:)
|
|
20
|
-
@argv = argv
|
|
21
|
-
@out = out
|
|
22
|
-
@err = err
|
|
23
|
-
end
|
|
24
|
-
|
|
25
21
|
# @return [Integer] CLI exit status.
|
|
26
22
|
def run
|
|
27
23
|
options = parse_options
|