rigortype 0.1.10 → 0.1.12
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/lib/rigor/analysis/baseline.rb +51 -15
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli.rb +44 -3
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +181 -4
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +190 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +189 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +81 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +142 -0
- data/plugins/rigor-actioncable/lib/rigor-actioncable.rb +3 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +199 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +398 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +86 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +183 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +713 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +201 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +226 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +261 -0
- data/plugins/rigor-actionpack/lib/rigor-actionpack.rb +3 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +114 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +177 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +65 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +117 -0
- data/plugins/rigor-activejob/lib/rigor-activejob.rb +3 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +283 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +114 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +561 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +194 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +250 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +98 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +590 -0
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +8 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +182 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +78 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +162 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +43 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +170 -0
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +8 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +37 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +20 -0
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +478 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +108 -0
- data/plugins/rigor-devise/lib/rigor-devise.rb +8 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +285 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +124 -0
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +8 -0
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +116 -0
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +8 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +341 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +120 -0
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +8 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +120 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +85 -0
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +7 -0
- data/plugins/rigor-dry-validation/sig/dry_validation.rbs +25 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +177 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +242 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +56 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +174 -0
- data/plugins/rigor-factorybot/lib/rigor-factorybot.rb +3 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +409 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +114 -0
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +8 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +124 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +111 -0
- data/plugins/rigor-hanami/lib/rigor-hanami.rb +3 -0
- data/plugins/rigor-hanami/sig/hanami_action.rbs +78 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +302 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +72 -0
- data/plugins/rigor-minitest/lib/rigor-minitest.rb +3 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +194 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +140 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +65 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +130 -0
- data/plugins/rigor-pundit/lib/rigor-pundit.rb +3 -0
- data/plugins/rigor-rails/lib/rigor-rails.rb +31 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +353 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +108 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +138 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +175 -0
- data/plugins/rigor-rails-i18n/lib/rigor-rails-i18n.rb +3 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +350 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +164 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1538 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +235 -0
- data/plugins/rigor-rails-routes/lib/rigor-rails-routes.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +163 -0
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +24 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +110 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +200 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +170 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +233 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +190 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +188 -0
- data/plugins/rigor-rspec/lib/rigor-rspec.rb +3 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +128 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +60 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +75 -0
- data/plugins/rigor-rspec-rails/lib/rigor-rspec-rails.rb +3 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +266 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +113 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor-shoulda-matchers.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +152 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +190 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +61 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +124 -0
- data/plugins/rigor-sidekiq/lib/rigor-sidekiq.rb +3 -0
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +85 -0
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +8 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +108 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +250 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +95 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +226 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +28 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +154 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +100 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +323 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +660 -0
- data/plugins/rigor-sorbet/lib/rigor-sorbet.rb +3 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +209 -0
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +8 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +163 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +9 -0
- data/sig/rigor/scope.rbs +22 -0
- metadata +157 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 182bad9de02b3b4579fe1c385fa740e30f2df85cad36d8c21647dfb09004b9eb
|
|
4
|
+
data.tar.gz: 398d4ebc670530522696122117592bd59b60d7f899ae0a8cd58b11c8506ec608
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abd25775ea4973023dc7ef7132669a0bf556604c7378caef60aa3c2fbae4ef79bc2651cd3499cecb8046507214dbdc4cb56bdb953c301ec42b0ebb86291202b6
|
|
7
|
+
data.tar.gz: 9281f16a4b2d39847aaaf408844920dfb9f2e6a914df3544182d6d3832f28f03ca63d8a19aa54ca80977f3431351717a59d7ba704f3361ee92697be21d6193ab
|
|
@@ -52,6 +52,15 @@ module Rigor
|
|
|
52
52
|
# CLI or `baseline: <path>` in `.rigor.yml`). The presence
|
|
53
53
|
# of `.rigor-baseline.yml` on disk alone never triggers a
|
|
54
54
|
# load — that's the CLI / Configuration's job to enforce.
|
|
55
|
+
#
|
|
56
|
+
# ## Path handling
|
|
57
|
+
#
|
|
58
|
+
# Baselines store file paths **relative to the project root**
|
|
59
|
+
# (the working directory when `rigor` is run). This makes the
|
|
60
|
+
# generated `.rigor-baseline.yml` portable across machines and
|
|
61
|
+
# checkout locations. When filtering a live diagnostic stream,
|
|
62
|
+
# the instance normalises each diagnostic's absolute path to a
|
|
63
|
+
# relative one before the bucket lookup.
|
|
55
64
|
class Baseline
|
|
56
65
|
# The bucket key is intentionally tuple-shaped so rule-ID
|
|
57
66
|
# rows and message-pattern rows can coexist in a single
|
|
@@ -69,12 +78,16 @@ module Rigor
|
|
|
69
78
|
# path is nil (the caller's "no baseline configured"
|
|
70
79
|
# state). Raises {LoadError} on malformed content;
|
|
71
80
|
# callers translate to a user-facing diagnostic.
|
|
72
|
-
|
|
81
|
+
#
|
|
82
|
+
# `project_root:` is the working directory against which
|
|
83
|
+
# stored relative paths are resolved during filtering.
|
|
84
|
+
# Defaults to `Dir.pwd`.
|
|
85
|
+
def load(path, project_root: Dir.pwd)
|
|
73
86
|
return nil if path.nil?
|
|
74
|
-
return new([]) unless File.exist?(path)
|
|
87
|
+
return new([], project_root: project_root) unless File.exist?(path)
|
|
75
88
|
|
|
76
89
|
raw = YAML.safe_load_file(path, permitted_classes: [Symbol])
|
|
77
|
-
parse_loaded(raw, path: path)
|
|
90
|
+
parse_loaded(raw, path: path, project_root: project_root)
|
|
78
91
|
end
|
|
79
92
|
|
|
80
93
|
# Build a baseline from a current run's diagnostic stream.
|
|
@@ -82,10 +95,14 @@ module Rigor
|
|
|
82
95
|
# message-mode generator passes literal messages through
|
|
83
96
|
# `Regexp.escape` so generated rows never accidentally
|
|
84
97
|
# over-match on punctuation.
|
|
85
|
-
|
|
98
|
+
#
|
|
99
|
+
# `project_root:` is used to convert absolute diagnostic
|
|
100
|
+
# paths to relative paths in the generated YAML. Defaults
|
|
101
|
+
# to `Dir.pwd`.
|
|
102
|
+
def from_diagnostics(diagnostics, match_mode: :rule, project_root: Dir.pwd)
|
|
86
103
|
raise ArgumentError, "match_mode must be :rule or :message" unless %i[rule message].include?(match_mode)
|
|
87
104
|
|
|
88
|
-
grouped = group_for_baseline(diagnostics, match_mode)
|
|
105
|
+
grouped = group_for_baseline(diagnostics, match_mode, project_root)
|
|
89
106
|
buckets = grouped.map do |key, entries|
|
|
90
107
|
Bucket.new(
|
|
91
108
|
file: key[0],
|
|
@@ -94,12 +111,12 @@ module Rigor
|
|
|
94
111
|
count: entries.size
|
|
95
112
|
)
|
|
96
113
|
end
|
|
97
|
-
new(buckets)
|
|
114
|
+
new(buckets, project_root: project_root)
|
|
98
115
|
end
|
|
99
116
|
|
|
100
117
|
private
|
|
101
118
|
|
|
102
|
-
def parse_loaded(raw, path:)
|
|
119
|
+
def parse_loaded(raw, path:, project_root:)
|
|
103
120
|
raise LoadError, "#{path}: expected a Hash at top level, got #{raw.class}" unless raw.is_a?(Hash)
|
|
104
121
|
|
|
105
122
|
version = raw["version"]
|
|
@@ -110,7 +127,8 @@ module Rigor
|
|
|
110
127
|
rows = raw["ignored"] || []
|
|
111
128
|
raise LoadError, "#{path}: `ignored:` must be an Array" unless rows.is_a?(Array)
|
|
112
129
|
|
|
113
|
-
new(rows.each_with_index.map { |row, idx| parse_row(row, path: path, index: idx) }
|
|
130
|
+
new(rows.each_with_index.map { |row, idx| parse_row(row, path: path, index: idx) },
|
|
131
|
+
project_root: project_root)
|
|
114
132
|
end
|
|
115
133
|
|
|
116
134
|
def parse_row(row, path:, index:)
|
|
@@ -141,16 +159,19 @@ module Rigor
|
|
|
141
159
|
# In message mode, each unique message gets its own bucket;
|
|
142
160
|
# in rule mode, every diagnostic for a (file, rule) pair
|
|
143
161
|
# contributes to a single bucket regardless of message.
|
|
144
|
-
|
|
162
|
+
# File paths are normalised to relative before keying.
|
|
163
|
+
def group_for_baseline(diagnostics, match_mode, project_root)
|
|
164
|
+
root = Pathname.new(project_root)
|
|
145
165
|
diagnostics.each_with_object({}) do |diag, into|
|
|
146
166
|
next if diag.qualified_rule.nil?
|
|
147
167
|
next if diag.path.nil?
|
|
148
168
|
|
|
169
|
+
rel = relative_path(diag.path, root)
|
|
149
170
|
key = case match_mode
|
|
150
171
|
when :rule
|
|
151
|
-
[
|
|
172
|
+
[rel, diag.qualified_rule, nil]
|
|
152
173
|
when :message
|
|
153
|
-
[
|
|
174
|
+
[rel, diag.qualified_rule, message_pattern_for(diag.message)]
|
|
154
175
|
end
|
|
155
176
|
(into[key] ||= []) << diag
|
|
156
177
|
end
|
|
@@ -164,13 +185,20 @@ module Rigor
|
|
|
164
185
|
def message_pattern_for(message)
|
|
165
186
|
Regexp.new(Regexp.escape(message.to_s))
|
|
166
187
|
end
|
|
188
|
+
|
|
189
|
+
def relative_path(path, root_pathname)
|
|
190
|
+
Pathname.new(path).relative_path_from(root_pathname).to_s
|
|
191
|
+
rescue ArgumentError
|
|
192
|
+
path # different drive letter on Windows or non-child path
|
|
193
|
+
end
|
|
167
194
|
end
|
|
168
195
|
|
|
169
196
|
class LoadError < StandardError; end
|
|
170
197
|
|
|
171
198
|
attr_reader :buckets
|
|
172
199
|
|
|
173
|
-
def initialize(buckets)
|
|
200
|
+
def initialize(buckets, project_root: Dir.pwd)
|
|
201
|
+
@project_root = Pathname.new(project_root)
|
|
174
202
|
@buckets = buckets.freeze
|
|
175
203
|
# For each (file, qualified_rule) pair, two arrays:
|
|
176
204
|
# - rule-ID rows (message_regex == nil)
|
|
@@ -264,7 +292,7 @@ module Rigor
|
|
|
264
292
|
# cleared buckets (`actual == 0`) from the on-disk file.
|
|
265
293
|
def without(buckets_to_drop)
|
|
266
294
|
dropset = buckets_to_drop.to_set
|
|
267
|
-
self.class.new(buckets.reject { |b| dropset.include?(b) })
|
|
295
|
+
self.class.new(buckets.reject { |b| dropset.include?(b) }, project_root: @project_root)
|
|
268
296
|
end
|
|
269
297
|
|
|
270
298
|
# Serialise to a YAML string. The generator path writes
|
|
@@ -307,6 +335,14 @@ module Rigor
|
|
|
307
335
|
[bucket.file, bucket.rule, bucket.message_regex&.source]
|
|
308
336
|
end
|
|
309
337
|
|
|
338
|
+
def normalize_path(path)
|
|
339
|
+
return path if path.nil?
|
|
340
|
+
|
|
341
|
+
Pathname.new(path).relative_path_from(@project_root).to_s
|
|
342
|
+
rescue ArgumentError
|
|
343
|
+
path # different drive letter on Windows or non-child path
|
|
344
|
+
end
|
|
345
|
+
|
|
310
346
|
def group_diagnostics_for_filtering(diagnostics)
|
|
311
347
|
# First pass: bin each diagnostic into the bucket that
|
|
312
348
|
# claims it. Message-pattern rows take precedence over
|
|
@@ -322,7 +358,7 @@ module Rigor
|
|
|
322
358
|
[bucket.file, bucket.rule,
|
|
323
359
|
bucket.message_regex&.source]
|
|
324
360
|
else
|
|
325
|
-
[diag.path, diag.qualified_rule, :__none__]
|
|
361
|
+
[normalize_path(diag.path), diag.qualified_rule, :__none__]
|
|
326
362
|
end
|
|
327
363
|
bin = (bins[key] ||= { bucket: bucket, diagnostics: [] })
|
|
328
364
|
bin[:diagnostics] << diag
|
|
@@ -331,7 +367,7 @@ module Rigor
|
|
|
331
367
|
end
|
|
332
368
|
|
|
333
369
|
def claim_bucket_for(diagnostic)
|
|
334
|
-
candidates = @by_pair[[diagnostic.path, diagnostic.qualified_rule]]
|
|
370
|
+
candidates = @by_pair[[normalize_path(diagnostic.path), diagnostic.qualified_rule]]
|
|
335
371
|
return nil if candidates.nil? || candidates.empty?
|
|
336
372
|
|
|
337
373
|
# Tighter (message-pattern) buckets first, then the
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# Detects when a `.rb` file is actually an ERB template — a Rails
|
|
6
|
+
# generator `templates/foo.rb` shape that uses `<%= ... %>` to
|
|
7
|
+
# interpolate identifiers. Prism rejects the template as invalid
|
|
8
|
+
# Ruby and the analyzer emits one parse-error diagnostic per scrap
|
|
9
|
+
# the parser tripped over (≈ 20 on Redmine's
|
|
10
|
+
# `lib/generators/redmine_plugin_model/templates/migration.rb`),
|
|
11
|
+
# all of them noise from the user's perspective. The runner / worker
|
|
12
|
+
# consults this detector before turning parse errors into
|
|
13
|
+
# diagnostics; an ERB-shaped source is silently skipped.
|
|
14
|
+
#
|
|
15
|
+
# Detection is byte-level on the raw source: any occurrence of the
|
|
16
|
+
# `%>` closing marker proves the file is an ERB template. `%>`
|
|
17
|
+
# cannot appear in valid Ruby — `%` is a binary operator that
|
|
18
|
+
# requires an operand on its right side, never `>`. The opening
|
|
19
|
+
# `<%` is ambiguous in principle (`x<%y` parses as `x < %y`, a
|
|
20
|
+
# comparison against a `%`-literal) but a real Ruby file with that
|
|
21
|
+
# shape would still not contain the closing `%>`.
|
|
22
|
+
module ErbTemplateDetector
|
|
23
|
+
ERB_CLOSING_MARKER = /%>/
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# @param parse_result [Prism::ParseResult]
|
|
28
|
+
# @return [Boolean] true when the parsed source looks like an
|
|
29
|
+
# ERB template (parse errors expected; analysis should skip).
|
|
30
|
+
def template?(parse_result)
|
|
31
|
+
source = parse_result.source.source
|
|
32
|
+
return false unless source.is_a?(String) && !source.empty?
|
|
33
|
+
|
|
34
|
+
ERB_CLOSING_MARKER.match?(source)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -20,6 +20,7 @@ require_relative "buffer_binding"
|
|
|
20
20
|
require_relative "check_rules"
|
|
21
21
|
require_relative "dependency_source_inference"
|
|
22
22
|
require_relative "diagnostic"
|
|
23
|
+
require_relative "erb_template_detector"
|
|
23
24
|
require_relative "project_scan"
|
|
24
25
|
require_relative "result"
|
|
25
26
|
require_relative "run_stats"
|
|
@@ -1457,7 +1458,11 @@ module Rigor
|
|
|
1457
1458
|
|
|
1458
1459
|
def analyze_file(path, environment) # rubocop:disable Metrics/MethodLength
|
|
1459
1460
|
parse_result = parse_source(path)
|
|
1460
|
-
|
|
1461
|
+
unless parse_result.errors.empty?
|
|
1462
|
+
return [] if ErbTemplateDetector.template?(parse_result)
|
|
1463
|
+
|
|
1464
|
+
return parse_diagnostics(path, parse_result)
|
|
1465
|
+
end
|
|
1461
1466
|
|
|
1462
1467
|
scope = seed_project_scope(Scope.empty(environment: environment, source_path: path))
|
|
1463
1468
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
@@ -15,6 +15,7 @@ require_relative "../inference/method_dispatcher/file_folding"
|
|
|
15
15
|
require_relative "check_rules"
|
|
16
16
|
require_relative "dependency_source_inference"
|
|
17
17
|
require_relative "diagnostic"
|
|
18
|
+
require_relative "erb_template_detector"
|
|
18
19
|
|
|
19
20
|
module Rigor
|
|
20
21
|
module Analysis
|
|
@@ -158,7 +159,11 @@ module Rigor
|
|
|
158
159
|
# is a per-run aggregate concern handled by the caller.
|
|
159
160
|
def analyze(path)
|
|
160
161
|
parse_result = parse_source(path)
|
|
161
|
-
|
|
162
|
+
unless parse_result.errors.empty?
|
|
163
|
+
return [] if ErbTemplateDetector.template?(parse_result)
|
|
164
|
+
|
|
165
|
+
return parse_diagnostics(path, parse_result)
|
|
166
|
+
end
|
|
162
167
|
|
|
163
168
|
scope = Scope.empty(environment: @environment, source_path: path)
|
|
164
169
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
@@ -84,7 +84,8 @@ module Rigor
|
|
|
84
84
|
configuration = Configuration.load(options.fetch(:config))
|
|
85
85
|
diagnostics = collect_diagnostics(configuration, options)
|
|
86
86
|
|
|
87
|
-
baseline = Analysis::Baseline.from_diagnostics(diagnostics, match_mode: options.fetch(:match_mode)
|
|
87
|
+
baseline = Analysis::Baseline.from_diagnostics(diagnostics, match_mode: options.fetch(:match_mode),
|
|
88
|
+
project_root: Dir.pwd)
|
|
88
89
|
File.write(path, baseline.to_yaml)
|
|
89
90
|
|
|
90
91
|
@err.puts(
|
|
@@ -149,7 +150,7 @@ module Rigor
|
|
|
149
150
|
defaults = Configuration::DEFAULTS.merge(
|
|
150
151
|
"paths" => configuration.paths,
|
|
151
152
|
"exclude" => configuration.exclude_patterns,
|
|
152
|
-
"plugins" => configuration.plugins
|
|
153
|
+
"plugins" => configuration.plugins,
|
|
153
154
|
"disable" => configuration.disabled_rules,
|
|
154
155
|
"libraries" => configuration.libraries,
|
|
155
156
|
"signature_paths" => configuration.signature_paths,
|
|
@@ -372,7 +373,7 @@ module Rigor
|
|
|
372
373
|
@err.puts("rigor: baseline file not found: #{path}")
|
|
373
374
|
return :error
|
|
374
375
|
end
|
|
375
|
-
Analysis::Baseline.load(path)
|
|
376
|
+
Analysis::Baseline.load(path, project_root: Dir.pwd)
|
|
376
377
|
rescue Analysis::Baseline::LoadError => e
|
|
377
378
|
@err.puts("rigor: baseline load failed: #{e.message}")
|
|
378
379
|
:error
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "../configuration"
|
|
6
|
+
require_relative "../plugin"
|
|
7
|
+
require_relative "../plugin/loader"
|
|
8
|
+
require_relative "../plugin/services"
|
|
9
|
+
require_relative "../reflection"
|
|
10
|
+
require_relative "../type/combinator"
|
|
11
|
+
require_relative "plugins_renderer"
|
|
12
|
+
|
|
13
|
+
module Rigor
|
|
14
|
+
class CLI
|
|
15
|
+
# `rigor plugins` — reports the activation status of every
|
|
16
|
+
# plugin entry in `.rigor.yml` so users (and the
|
|
17
|
+
# `rigor-project-init` SKILL, CI gates, `rigor init`) can
|
|
18
|
+
# verify their plugin configuration is actually doing what
|
|
19
|
+
# they think.
|
|
20
|
+
#
|
|
21
|
+
# The command is read-only and idempotent: it loads the
|
|
22
|
+
# project's `.rigor.yml` (same discovery as `rigor check`),
|
|
23
|
+
# runs `Plugin::Loader.load` to attempt instantiation, then
|
|
24
|
+
# prints a table of:
|
|
25
|
+
#
|
|
26
|
+
# - load status (`loaded` / `load-error` with reason);
|
|
27
|
+
# - resolved manifest id, version, description;
|
|
28
|
+
# - `signature_paths:` (absolute paths + per-dir `.rbs` count);
|
|
29
|
+
# - every manifest-declared extension surface
|
|
30
|
+
# (`open_receivers:` / `owns_receivers:` / `produces:` /
|
|
31
|
+
# `consumes:` / `block_as_methods:` / `heredoc_templates:` /
|
|
32
|
+
# `trait_registries:` / `external_files:` /
|
|
33
|
+
# `type_node_resolvers:` / `hkt_registrations:` /
|
|
34
|
+
# `hkt_definitions:` / `protocol_contracts:` /
|
|
35
|
+
# `source_rbs_synthesizer:`).
|
|
36
|
+
#
|
|
37
|
+
# Output formats: `text` (default, human-readable table) and
|
|
38
|
+
# `json` (for tooling — SKILLs, CI gates, editor integrations).
|
|
39
|
+
#
|
|
40
|
+
# Exit codes:
|
|
41
|
+
# - `0` — every configured plugin loaded.
|
|
42
|
+
# - `1` — at least one plugin failed to load AND `--strict`
|
|
43
|
+
# was passed. Without `--strict` the command always exits 0;
|
|
44
|
+
# load errors are reported but not treated as a gate failure
|
|
45
|
+
# (matching `rigor triage`'s advisory shape).
|
|
46
|
+
#
|
|
47
|
+
# Future expansion (not in this slice):
|
|
48
|
+
# - Per-plugin diagnostic counts (would require running the
|
|
49
|
+
# full analysis pipeline; out of scope for an inspection
|
|
50
|
+
# command).
|
|
51
|
+
# - Verification that `signature_paths` actually merged into
|
|
52
|
+
# the RBS environment without conflict (requires constructing
|
|
53
|
+
# the Environment, which is heavier than the loader-only
|
|
54
|
+
# pass this slice does).
|
|
55
|
+
class PluginsCommand
|
|
56
|
+
USAGE = "Usage: rigor plugins [options]"
|
|
57
|
+
|
|
58
|
+
def initialize(argv:, out: $stdout, err: $stderr)
|
|
59
|
+
@argv = argv
|
|
60
|
+
@out = out
|
|
61
|
+
@err = err
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Integer] CLI exit status.
|
|
65
|
+
def run
|
|
66
|
+
options = parse_options
|
|
67
|
+
config_path = options.fetch(:config) || Configuration.discover
|
|
68
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
69
|
+
rows = build_rows(configuration)
|
|
70
|
+
|
|
71
|
+
renderer = PluginsRenderer.new(rows: rows, configuration_path: config_path)
|
|
72
|
+
@out.puts(options.fetch(:format) == "json" ? renderer.json : renderer.text)
|
|
73
|
+
|
|
74
|
+
any_load_errors = rows.any? { |row| row.fetch(:status) == :load_error }
|
|
75
|
+
return 1 if any_load_errors && options.fetch(:strict)
|
|
76
|
+
|
|
77
|
+
0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def parse_options
|
|
83
|
+
options = { config: nil, format: "text", strict: false }
|
|
84
|
+
OptionParser.new do |opts|
|
|
85
|
+
opts.banner = USAGE
|
|
86
|
+
opts.on("--config=PATH", "Path to the Rigor configuration file") { |v| options[:config] = v }
|
|
87
|
+
opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
|
|
88
|
+
opts.on("--strict", "Exit 1 if any plugin failed to load (CI gate)") { options[:strict] = true }
|
|
89
|
+
end.parse!(@argv)
|
|
90
|
+
validate!(options)
|
|
91
|
+
options
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate!(options)
|
|
95
|
+
return if %w[text json].include?(options.fetch(:format))
|
|
96
|
+
|
|
97
|
+
raise OptionParser::InvalidArgument, "unsupported format: #{options.fetch(:format)}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Runs the plugin loader against the project configuration
|
|
101
|
+
# and returns a row per declared plugin entry. Each row is
|
|
102
|
+
# a plain Hash with the fields enumerated in the class
|
|
103
|
+
# docstring so the renderer (text + JSON) reads from a
|
|
104
|
+
# single shape.
|
|
105
|
+
#
|
|
106
|
+
# The loader catches require / init failures and surfaces
|
|
107
|
+
# them through `registry.load_errors`; we merge those back
|
|
108
|
+
# into the per-entry rows by matching on `plugin_ref`.
|
|
109
|
+
def build_rows(configuration)
|
|
110
|
+
services = build_services(configuration)
|
|
111
|
+
registry = Plugin::Loader.load(configuration: configuration, services: services)
|
|
112
|
+
|
|
113
|
+
rows = configuration.plugins.map { |entry| row_for_entry(entry, registry) }
|
|
114
|
+
# Surface registry-level errors that did not bind to an
|
|
115
|
+
# entry (e.g. dependency-cycle errors that name multiple
|
|
116
|
+
# plugins). The renderer treats these as "orphan" errors.
|
|
117
|
+
orphan_errors = orphan_load_errors(registry, rows)
|
|
118
|
+
rows + orphan_errors
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_services(configuration)
|
|
122
|
+
Plugin::Services.new(
|
|
123
|
+
reflection: Reflection,
|
|
124
|
+
type: Type::Combinator,
|
|
125
|
+
configuration: configuration,
|
|
126
|
+
cache_store: nil
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def row_for_entry(entry, registry)
|
|
131
|
+
gem_name = entry_gem_name(entry)
|
|
132
|
+
config = entry_config(entry)
|
|
133
|
+
plugin = registry.plugins.find do |p|
|
|
134
|
+
# The loader has already matched the entry to a plugin
|
|
135
|
+
# class; we can identify it by either the gem name (when
|
|
136
|
+
# the entry was a String) or the explicit id (when the
|
|
137
|
+
# entry was a Hash with `id:`).
|
|
138
|
+
plugin_matches_entry?(p, gem_name, entry_id(entry))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if plugin
|
|
142
|
+
loaded_row(plugin, gem_name, config)
|
|
143
|
+
else
|
|
144
|
+
# Find the load error whose plugin_ref names this entry
|
|
145
|
+
# (the ref is set by Loader to the gem name on require
|
|
146
|
+
# failures and to the manifest id on later failures).
|
|
147
|
+
error = registry.load_errors.find do |e|
|
|
148
|
+
ref = e.plugin_ref.to_s
|
|
149
|
+
ref == gem_name || ref == entry_id(entry).to_s
|
|
150
|
+
end
|
|
151
|
+
load_error_row(gem_name, entry_id(entry), config, error)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def plugin_matches_entry?(plugin, gem_name, entry_id)
|
|
156
|
+
return true if entry_id && plugin.manifest.id == entry_id
|
|
157
|
+
|
|
158
|
+
# A bare gem entry conventionally has manifest.id equal to
|
|
159
|
+
# the gem name with the `rigor-` prefix stripped.
|
|
160
|
+
derived_id = gem_name.delete_prefix("rigor-")
|
|
161
|
+
[derived_id, gem_name].include?(plugin.manifest.id)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def loaded_row(plugin, gem_name, config)
|
|
165
|
+
manifest = plugin.manifest
|
|
166
|
+
identity_fields(gem_name, manifest, config)
|
|
167
|
+
.merge(extension_fields(plugin, manifest))
|
|
168
|
+
.merge(load_error: nil)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def identity_fields(gem_name, manifest, config)
|
|
172
|
+
{
|
|
173
|
+
gem: gem_name,
|
|
174
|
+
status: :loaded,
|
|
175
|
+
id: manifest.id,
|
|
176
|
+
version: manifest.version,
|
|
177
|
+
description: manifest.description,
|
|
178
|
+
config: config
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def extension_fields(plugin, manifest)
|
|
183
|
+
{
|
|
184
|
+
signature_paths: signature_path_rows(plugin),
|
|
185
|
+
open_receivers: manifest.open_receivers.dup,
|
|
186
|
+
owns_receivers: manifest.owns_receivers.dup,
|
|
187
|
+
produces: manifest.produces.map(&:to_s),
|
|
188
|
+
consumes: manifest.consumes.map { |c| "#{c.plugin_id}/#{c.name}#{'?' if c.optional}" },
|
|
189
|
+
source_rbs_synthesizer: !manifest.source_rbs_synthesizer.nil?
|
|
190
|
+
}.merge(extension_count_fields(manifest))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def extension_count_fields(manifest)
|
|
194
|
+
{
|
|
195
|
+
block_as_methods: manifest.block_as_methods.size,
|
|
196
|
+
heredoc_templates: manifest.heredoc_templates.size,
|
|
197
|
+
trait_registries: manifest.trait_registries.size,
|
|
198
|
+
external_files: manifest.external_files.size,
|
|
199
|
+
type_node_resolvers: manifest.type_node_resolvers.size,
|
|
200
|
+
hkt_registrations: manifest.hkt_registrations.size,
|
|
201
|
+
hkt_definitions: manifest.hkt_definitions.size,
|
|
202
|
+
protocol_contracts: manifest.protocol_contracts.size
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def load_error_row(gem_name, entry_id, config, error)
|
|
207
|
+
{
|
|
208
|
+
gem: gem_name,
|
|
209
|
+
status: :load_error,
|
|
210
|
+
id: entry_id,
|
|
211
|
+
version: nil,
|
|
212
|
+
description: nil,
|
|
213
|
+
config: config,
|
|
214
|
+
signature_paths: [],
|
|
215
|
+
open_receivers: [],
|
|
216
|
+
owns_receivers: [],
|
|
217
|
+
produces: [],
|
|
218
|
+
consumes: [],
|
|
219
|
+
block_as_methods: 0,
|
|
220
|
+
heredoc_templates: 0,
|
|
221
|
+
trait_registries: 0,
|
|
222
|
+
external_files: 0,
|
|
223
|
+
type_node_resolvers: 0,
|
|
224
|
+
hkt_registrations: 0,
|
|
225
|
+
hkt_definitions: 0,
|
|
226
|
+
protocol_contracts: 0,
|
|
227
|
+
source_rbs_synthesizer: false,
|
|
228
|
+
load_error: error&.message || "plugin did not register or could not be matched to a registered class"
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# For each `signature_paths:` directory the plugin resolves
|
|
233
|
+
# against its gem root, report the absolute path and a
|
|
234
|
+
# quick `.rbs`-file count. The count is a sanity signal:
|
|
235
|
+
# an empty bundle directory loads silently today but
|
|
236
|
+
# contributes nothing.
|
|
237
|
+
def signature_path_rows(plugin)
|
|
238
|
+
plugin.signature_paths.map do |abs|
|
|
239
|
+
{
|
|
240
|
+
path: abs,
|
|
241
|
+
exists: File.directory?(abs),
|
|
242
|
+
rbs_files: rbs_file_count(abs)
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def rbs_file_count(dir)
|
|
248
|
+
return 0 unless File.directory?(dir)
|
|
249
|
+
|
|
250
|
+
Dir.glob(File.join(dir, "**", "*.rbs")).size
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def entry_gem_name(entry)
|
|
254
|
+
case entry
|
|
255
|
+
when String then entry
|
|
256
|
+
when Hash
|
|
257
|
+
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
258
|
+
(string_keyed["gem"] || string_keyed["id"]).to_s
|
|
259
|
+
else entry.to_s
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def entry_id(entry)
|
|
264
|
+
case entry
|
|
265
|
+
when Hash
|
|
266
|
+
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
267
|
+
string_keyed["id"]
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def entry_config(entry)
|
|
272
|
+
case entry
|
|
273
|
+
when Hash
|
|
274
|
+
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
275
|
+
string_keyed["config"] || {}
|
|
276
|
+
else {}
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Build orphan-error rows for load errors whose `plugin_ref`
|
|
281
|
+
# did not bind to any configured plugin entry (e.g. a
|
|
282
|
+
# dependency-cycle naming a plugin we already accounted for
|
|
283
|
+
# but reported alongside another). De-duplicates against
|
|
284
|
+
# errors we already attached.
|
|
285
|
+
def orphan_load_errors(registry, rows)
|
|
286
|
+
attached_refs = rows.compact.flat_map { |row| [row[:gem], row[:id]].compact }.to_set
|
|
287
|
+
unattached = registry.load_errors.reject { |error| attached_refs.include?(error.plugin_ref.to_s) }
|
|
288
|
+
unattached.map do |error|
|
|
289
|
+
{
|
|
290
|
+
gem: error.plugin_ref.to_s,
|
|
291
|
+
status: :load_error,
|
|
292
|
+
id: nil,
|
|
293
|
+
version: nil,
|
|
294
|
+
description: nil,
|
|
295
|
+
config: {},
|
|
296
|
+
signature_paths: [],
|
|
297
|
+
open_receivers: [], owns_receivers: [], produces: [], consumes: [],
|
|
298
|
+
block_as_methods: 0, heredoc_templates: 0, trait_registries: 0,
|
|
299
|
+
external_files: 0, type_node_resolvers: 0,
|
|
300
|
+
hkt_registrations: 0, hkt_definitions: 0,
|
|
301
|
+
protocol_contracts: 0, source_rbs_synthesizer: false,
|
|
302
|
+
load_error: error.message
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|