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
|
@@ -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,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
|
|
@@ -30,7 +30,7 @@ module Rigor
|
|
|
30
30
|
# - every manifest-declared extension surface
|
|
31
31
|
# (`open_receivers:` / `owns_receivers:` / `produces:` /
|
|
32
32
|
# `consumes:` / `block_as_methods:` / `heredoc_templates:` /
|
|
33
|
-
# `trait_registries:` /
|
|
33
|
+
# `trait_registries:` /
|
|
34
34
|
# `type_node_resolvers:` / `hkt_registrations:` /
|
|
35
35
|
# `hkt_definitions:` / `protocol_contracts:` /
|
|
36
36
|
# `source_rbs_synthesizer:`);
|
|
@@ -233,7 +233,6 @@ module Rigor
|
|
|
233
233
|
block_as_methods: manifest.block_as_methods.size,
|
|
234
234
|
heredoc_templates: manifest.heredoc_templates.size,
|
|
235
235
|
trait_registries: manifest.trait_registries.size,
|
|
236
|
-
external_files: manifest.external_files.size,
|
|
237
236
|
type_node_resolvers: manifest.type_node_resolvers.size,
|
|
238
237
|
hkt_registrations: manifest.hkt_registrations.size,
|
|
239
238
|
hkt_definitions: manifest.hkt_definitions.size,
|
|
@@ -257,7 +256,6 @@ module Rigor
|
|
|
257
256
|
block_as_methods: 0,
|
|
258
257
|
heredoc_templates: 0,
|
|
259
258
|
trait_registries: 0,
|
|
260
|
-
external_files: 0,
|
|
261
259
|
type_node_resolvers: 0,
|
|
262
260
|
hkt_registrations: 0,
|
|
263
261
|
hkt_definitions: 0,
|
|
@@ -337,7 +335,7 @@ module Rigor
|
|
|
337
335
|
signature_paths: [],
|
|
338
336
|
open_receivers: [], owns_receivers: [], produces: [], consumes: [],
|
|
339
337
|
block_as_methods: 0, heredoc_templates: 0, trait_registries: 0,
|
|
340
|
-
|
|
338
|
+
type_node_resolvers: 0,
|
|
341
339
|
hkt_registrations: 0, hkt_definitions: 0,
|
|
342
340
|
protocol_contracts: 0, source_rbs_synthesizer: false,
|
|
343
341
|
node_rule_types: [], dynamic_return_receivers: [], type_specifier_methods: [],
|
|
@@ -209,7 +209,6 @@ module Rigor
|
|
|
209
209
|
parts << "block_as_methods=#{row[:block_as_methods]}" if row[:block_as_methods].positive?
|
|
210
210
|
parts << "heredoc_templates=#{row[:heredoc_templates]}" if row[:heredoc_templates].positive?
|
|
211
211
|
parts << "trait_registries=#{row[:trait_registries]}" if row[:trait_registries].positive?
|
|
212
|
-
parts << "external_files=#{row[:external_files]}" if row[:external_files].positive?
|
|
213
212
|
return [] if parts.empty?
|
|
214
213
|
|
|
215
214
|
[" macro substrate: #{parts.join(', ')}"]
|
|
@@ -233,7 +232,6 @@ module Rigor
|
|
|
233
232
|
"block_as_methods" => row[:block_as_methods],
|
|
234
233
|
"heredoc_templates" => row[:heredoc_templates],
|
|
235
234
|
"trait_registries" => row[:trait_registries],
|
|
236
|
-
"external_files" => row[:external_files],
|
|
237
235
|
"type_node_resolvers" => row[:type_node_resolvers],
|
|
238
236
|
"hkt_registrations" => row[:hkt_registrations],
|
|
239
237
|
"hkt_definitions" => row[:hkt_definitions],
|
|
@@ -41,12 +41,19 @@ module Rigor
|
|
|
41
41
|
# @return [String] the source with ANSI colour escapes, or
|
|
42
42
|
# the input unchanged when lexing surfaces an error.
|
|
43
43
|
def colorize(source)
|
|
44
|
+
# Sources read under a POSIX locale arrive tagged US-ASCII even
|
|
45
|
+
# when they carry UTF-8 bytes; retag so the token regexes below
|
|
46
|
+
# do not raise on multibyte comments.
|
|
47
|
+
source = source.dup.force_encoding(Encoding::UTF_8) unless source.encoding == Encoding::UTF_8
|
|
44
48
|
result = Prism.lex(source)
|
|
45
49
|
return source unless result.errors.empty?
|
|
46
50
|
|
|
47
51
|
render(source, result.value)
|
|
48
52
|
end
|
|
49
53
|
|
|
54
|
+
# Prism token offsets are BYTE offsets — slice with byteslice, or
|
|
55
|
+
# any multibyte character earlier in the source shifts every
|
|
56
|
+
# subsequent token boundary.
|
|
50
57
|
def render(source, lexed)
|
|
51
58
|
out = +""
|
|
52
59
|
offset = 0
|
|
@@ -54,15 +61,15 @@ module Rigor
|
|
|
54
61
|
lexed.each do |entry|
|
|
55
62
|
token = entry.first
|
|
56
63
|
location = token.location
|
|
57
|
-
out << source[
|
|
64
|
+
out << (source.byteslice(offset, [location.start_offset - offset, 0].max) || "")
|
|
58
65
|
break if token.type == :EOF
|
|
59
66
|
|
|
60
|
-
text = source
|
|
67
|
+
text = source.byteslice(location.start_offset, location.end_offset - location.start_offset) || ""
|
|
61
68
|
out << paint(text, effective_category(token.type, previous_type))
|
|
62
69
|
offset = location.end_offset
|
|
63
70
|
previous_type = token.type
|
|
64
71
|
end
|
|
65
|
-
out << (source
|
|
72
|
+
out << (source.byteslice(offset, source.bytesize - offset) || "")
|
|
66
73
|
out
|
|
67
74
|
end
|
|
68
75
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optionparser"
|
|
5
|
+
|
|
6
|
+
require_relative "../bleeding_edge"
|
|
7
|
+
require_relative "../configuration"
|
|
8
|
+
require_relative "command"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
class CLI
|
|
12
|
+
# Executes `rigor show-bleedingedge` (ADR-50 § WD2).
|
|
13
|
+
#
|
|
14
|
+
# Prints the bleeding-edge overlay — the Rigor-maintained set of the
|
|
15
|
+
# next major's queued changes ({Rigor::BleedingEdge}) — as an
|
|
16
|
+
# explicit list, and reports which of them the project's
|
|
17
|
+
# `bleeding_edge:` configuration adopts. The overlay is empty in this
|
|
18
|
+
# release, so the command currently reports an empty set; it becomes
|
|
19
|
+
# the inspection surface ADR-50 describes once a feature is queued.
|
|
20
|
+
#
|
|
21
|
+
# Read-only: it loads `.rigor.yml` to resolve the active selection
|
|
22
|
+
# but runs no analysis.
|
|
23
|
+
class ShowBleedingedgeCommand < Command
|
|
24
|
+
USAGE = "Usage: rigor show-bleedingedge [options]"
|
|
25
|
+
|
|
26
|
+
# @return [Integer] CLI exit status.
|
|
27
|
+
def run
|
|
28
|
+
options = parse_options
|
|
29
|
+
configuration = load_configuration(options)
|
|
30
|
+
return CLI::EXIT_USAGE if configuration.nil?
|
|
31
|
+
|
|
32
|
+
case options.fetch(:format)
|
|
33
|
+
when "json" then render_json(configuration)
|
|
34
|
+
else render_text(configuration)
|
|
35
|
+
end
|
|
36
|
+
0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def parse_options
|
|
42
|
+
options = { format: "text", config: nil }
|
|
43
|
+
OptionParser.new do |opt|
|
|
44
|
+
opt.banner = USAGE
|
|
45
|
+
opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
|
|
46
|
+
options[:format] = fmt
|
|
47
|
+
end
|
|
48
|
+
opt.on("--config=PATH", "Path to a .rigor.yml (default: auto-discovery).") do |path|
|
|
49
|
+
options[:config] = path
|
|
50
|
+
end
|
|
51
|
+
end.parse!(@argv)
|
|
52
|
+
options
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_configuration(options)
|
|
56
|
+
Configuration.load(options.fetch(:config))
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
@err.puts("show-bleedingedge: could not load configuration: #{e.message}")
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_json(configuration)
|
|
63
|
+
selector = configuration.bleeding_edge
|
|
64
|
+
@out.puts(JSON.pretty_generate(
|
|
65
|
+
"overlay" => BleedingEdge.features.map(&:to_h),
|
|
66
|
+
"selector" => configuration.to_h.fetch("bleeding_edge"),
|
|
67
|
+
"active" => BleedingEdge.active_features(selector).map(&:id),
|
|
68
|
+
"unknown_selected" => BleedingEdge.unknown_selected_ids(selector)
|
|
69
|
+
))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_text(configuration)
|
|
73
|
+
@out.puts("Bleeding-edge overlay (ADR-50 § WD2)")
|
|
74
|
+
@out.puts("")
|
|
75
|
+
if BleedingEdge.features.empty?
|
|
76
|
+
render_empty_overlay
|
|
77
|
+
else
|
|
78
|
+
render_overlay
|
|
79
|
+
end
|
|
80
|
+
@out.puts("")
|
|
81
|
+
render_selection(configuration)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_empty_overlay
|
|
85
|
+
@out.puts("The overlay is empty in this release — no features are queued for")
|
|
86
|
+
@out.puts("the next major. The `bleeding_edge:` mechanism is wired and ready;")
|
|
87
|
+
@out.puts("there is simply nothing to adopt yet.")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_overlay
|
|
91
|
+
@out.puts("#{BleedingEdge.features.length} feature(s) queued for the next major:")
|
|
92
|
+
@out.puts("")
|
|
93
|
+
BleedingEdge.features.each do |feature|
|
|
94
|
+
@out.puts(" #{feature.id}")
|
|
95
|
+
@out.puts(" #{feature.summary}")
|
|
96
|
+
feature.severity_overrides.each do |rule, severity|
|
|
97
|
+
@out.puts(" severity: #{rule} → :#{severity}")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def render_selection(configuration)
|
|
103
|
+
selector = configuration.bleeding_edge
|
|
104
|
+
active = BleedingEdge.active_features(selector).map(&:id)
|
|
105
|
+
@out.puts("Your configuration adopts: #{active.empty? ? '(none)' : active.join(', ')}")
|
|
106
|
+
|
|
107
|
+
unknown = BleedingEdge.unknown_selected_ids(selector)
|
|
108
|
+
return if unknown.empty?
|
|
109
|
+
|
|
110
|
+
@out.puts("Selected but not in this overlay (ignored): #{unknown.join(', ')}")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|