rigortype 0.2.2 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a23952ba800fa7d807c8b8acc4d1cfbb3489024d91c3c30d687ebfdb75c7d1f5
4
- data.tar.gz: 1a301a820c1c0bd3c2946695ae59f8e00906d1ecebedc0fbfb1fa93da3f6a72a
3
+ metadata.gz: 749e0e15c9b85dcec97c9afc1b107d5ebc1a1a685cc19aa39d316a9826992d4a
4
+ data.tar.gz: 45bcee26284594e35b51a050ce8ac234935bee954063b50fdfe1b0c6a9bcddea
5
5
  SHA512:
6
- metadata.gz: 6574512ee06d1eb87e42f697d7261eac8ecf9bba9ae4a8a4be6263fba188de5b9786babdcef7e9b2cf41ac4877010726d60f66b741af7b5295d6d3e98e5937cb
7
- data.tar.gz: d1bcccf23cac521597657aadd2f1c35d7a860998dd3fc8e02a7802c2dc0e9bf23ca3c0e6c368cdb9574cc1ac1e5ff2395f9dd5f164beae13541874d617886b19
6
+ metadata.gz: fbf022b6e92a96e1c095cb3d887936161e836201662f051c60de5d0d78494f263228a28df509c3b937768735b5ce18162ee28657b2aaf1cd7a8b2d4851338a53
7
+ data.tar.gz: a49e89a07950f54fcf8f38e328b8fdb658aaf0802154df457c7b9b1370f35279c1d6d5fe3acf6a45c9e49ff2f7c09ff9f209b1f7dab68d20b283c9baa4b2760b
@@ -232,6 +232,18 @@ rigor triage [paths]
232
232
  `--selectors-only`, and `--no-hints` select which sections print.
233
233
  `triage` is advisory and always exits `0` — it never gates a build.
234
234
 
235
+ By default the distribution, selectors, and hotspots sections count
236
+ only the **actionable** diagnostics (`error` + `warning`). `info`
237
+ diagnostics are excluded from these volume views — on a Rails project
238
+ they are dominated by plugin *recognition trace* (`Account.find
239
+ resolves to Account`, `users_path → GET /users`), positive "Rigor
240
+ resolved this" records that would otherwise bury the genuine signal
241
+ and skew the hotspot ranking toward the files with the most *working*
242
+ code. The summary line still reports the full `info` count, and
243
+ heuristic hints still see every diagnostic (so the `gem-without-rbs`
244
+ notice survives). Pass `--include-info` to route `info` into the
245
+ volume views as well.
246
+
235
247
  The **`selectors`** section is the by-(class, method) axis: it
236
248
  aggregates the structured `receiver_type` / `method_name` fields the
237
249
  diagnostics carry into `{receiver, method, count, files, rules}`
@@ -295,10 +295,17 @@ the most common error clusters.
295
295
  "hints": [
296
296
  { "id": "activesupport-core-ext", "confidence": "likely",
297
297
  "diagnostic_count": 365, "summary": "...", "action": "..." }
298
- ]
298
+ ],
299
+ "include_info": false
299
300
  }
300
301
  ```
301
302
 
303
+ By default the `distribution` / `selectors` / `hotspots` arrays count
304
+ only the actionable diagnostics (`error` + `warning`); `info` (mostly
305
+ plugin recognition trace) is excluded, so those counts do not sum to
306
+ `summary.total` and `include_info` is `false`. `summary.info` still
307
+ reports the full info count, and the hints still see every diagnostic.
308
+
302
309
  The `selectors` array is the by-(class, method) axis: each row is a
303
310
  dispatch target with its `count`, distinct-`files` spread, and
304
311
  per-`rule` breakdown, keyed on a normalised receiver class — so an
@@ -47,6 +47,9 @@ module Rigor
47
47
  def initialize(path:, line:, column:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
48
48
  source_family: DEFAULT_SOURCE_FAMILY,
49
49
  receiver_type: nil, method_name: nil, project_definition_site: nil)
50
+ raise ArgumentError, "line must be >= 1, got #{line}" if line < 1
51
+ raise ArgumentError, "column must be >= 1, got #{column}" if column < 1
52
+
50
53
  @path = path
51
54
  @line = line
52
55
  @column = column
@@ -208,8 +208,14 @@ module Rigor
208
208
  ["rigor-rbs-setup", "your gems ship no community RBS yet — install it so Rigor stops typing them as Dynamic."]
209
209
  elsif state.fetch(:ci) != :wired
210
210
  ["rigor-ci-setup", "Rigor is configured but not wired into CI — lock in the regression guard."]
211
- elsif state.fetch(:baseline)
212
- ["rigor-baseline-reduce", "a baseline is in place work it down rule by rule."]
211
+ # A present baseline is deliberately NOT a recommendation trigger.
212
+ # A baseline is a healthy, finished onboarding state, not a problem to
213
+ # work off; pushing every baselined project to "reduce it" turns a
214
+ # working build into a chore and tempts scattering `# rigor:disable`
215
+ # through the code to make a number go down — means over ends.
216
+ # `rigor-baseline-reduce` stays in the catalogue for the intermediate
217
+ # user who *chooses* to invest in it; the headline routes to genuinely
218
+ # additive steps instead.
213
219
  elsif state.fetch(:editor) == :unwired
214
220
  ["rigor-editor-setup",
215
221
  "you have an editor config but no Rigor LSP — wire `rigor lsp` for live diagnostics and hover types."]
@@ -297,7 +303,6 @@ module Rigor
297
303
  The recommendation above is from a presence-only probe — it does not run
298
304
  `rigor check`. If you have run (or now run) `rigor check`, let its findings
299
305
  refine the choice:
300
- - errors present and no baseline yet → rigor-baseline-reduce
301
306
  - a `call.unresolved-toplevel` / `call.undefined-method` cluster on the
302
307
  project's own monkey-patches → rigor-monkeypatch-resolve
303
308
  - framework calls (ActiveRecord, routes, i18n …) typing as Dynamic with no
@@ -31,7 +31,8 @@ module Rigor
31
31
  diagnostics = analyze(configuration)
32
32
 
33
33
  report = Triage.analyze(diagnostics, top: options.fetch(:top),
34
- hints: options.fetch(:sections).include?(:hints))
34
+ hints: options.fetch(:sections).include?(:hints),
35
+ include_info: options.fetch(:include_info))
35
36
  renderer = TriageRenderer.new(report, sections: options.fetch(:sections))
36
37
  @out.puts(options.fetch(:format) == "json" ? renderer.json : renderer.text)
37
38
  0
@@ -40,12 +41,17 @@ module Rigor
40
41
  private
41
42
 
42
43
  def parse_options
43
- options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS }
44
+ options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS, include_info: false }
44
45
  OptionParser.new do |opts|
45
46
  opts.banner = USAGE
46
47
  Options.add_config(opts, options)
47
48
  opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
48
49
  opts.on("--top=N", Integer, "Hotspot-file count (default 10)") { |v| options[:top] = v }
50
+ opts.on("--include-info",
51
+ "Route info diagnostics into distribution / selectors / hotspots " \
52
+ "(excluded by default — mostly plugin recognition trace)") do
53
+ options[:include_info] = true
54
+ end
49
55
  opts.on("--hints-only", "Print only the heuristic-hints section") { options[:sections] = %i[hints] }
50
56
  opts.on("--no-hints", "Print distribution + selectors + hotspots only") do
51
57
  options[:sections] = %i[distribution selectors hotspots]
@@ -37,6 +37,10 @@ module Rigor
37
37
  max = @report.distribution.map(&:count).max || 1
38
38
  lines = ["Diagnostic distribution — #{s.total} total " \
39
39
  "(#{s.error} error / #{s.warning} warning#{" / #{s.info} info" if s.info.positive?})"]
40
+ if !@report.include_info && s.info.positive?
41
+ lines << " #{s.info} info diagnostic(s) hidden below " \
42
+ "(mostly plugin recognition trace) — pass --include-info to route them"
43
+ end
40
44
  @report.distribution.each do |row|
41
45
  lines << format(" %<rule>-32s %<count>5d %<bar>s",
42
46
  rule: row.rule, count: row.count, bar: bar(row.count, max))
@@ -170,7 +170,7 @@ module Rigor
170
170
  source = missing.map { |name| "module #{name}\nend\n" }.join
171
171
  buffer = ::RBS::Buffer.new(name: SYNTHETIC_NAMESPACE_BUFFER, content: source)
172
172
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
173
- env.add_source(::RBS::Source::RBS.new(buffer, directives || [], decls || []))
173
+ add_parsed_decls(env, buffer, directives, decls)
174
174
  rescue ::RBS::BaseError
175
175
  # Fail-soft: synthesis is an opportunistic uplift, never a
176
176
  # hard requirement. A parse failure here just leaves the env
@@ -232,11 +232,52 @@ module Rigor
232
232
  missing.uniq
233
233
  end
234
234
 
235
+ # Normalises a `class_decls` entry's representative declaration
236
+ # across the gemspec's supported RBS range (`rbs >= 3.0, < 5.0`).
237
+ # RBS 4.x exposes it as `entry.primary_decl` (the AST declaration
238
+ # directly); RBS 3.x exposes `entry.primary` (a wrapper whose
239
+ # `#decl` is the AST declaration). Returns the AST declaration, or
240
+ # nil when neither accessor is present. Without this guard,
241
+ # `class_decl_paths` crashed under RBS 3.x with
242
+ # `undefined method 'primary_decl'`.
243
+ def primary_decl_for(entry)
244
+ if entry.respond_to?(:primary_decl)
245
+ entry.primary_decl
246
+ elsif entry.respond_to?(:primary)
247
+ primary = entry.primary
248
+ primary.respond_to?(:decl) ? primary.decl : primary
249
+ end
250
+ end
251
+
252
+ # Appends freshly-parsed declarations to an `RBS::Environment`
253
+ # across the gemspec's supported RBS range (`rbs >= 3.0, < 5.0`).
254
+ # RBS 4.x wraps the declarations in an `RBS::Source::RBS` and
255
+ # takes them through `env.add_source`; RBS 3.x has neither
256
+ # `RBS::Source` nor `add_source` and instead registers them with
257
+ # `env.add_signature(buffer:, directives:, decls:)` (a bare
258
+ # `env << decl` is NOT enough — it skips the `signatures` table
259
+ # that `resolve_type_names` rebuilds from, so the synthesized
260
+ # declarations silently vanish on resolve). Without this guard
261
+ # the synthesis paths (`synthesize_missing_namespaces`,
262
+ # `append_stub_declarations`, `add_virtual_rbs`) crashed under
263
+ # RBS 3.x with `uninitialized constant RBS::Source`.
264
+ def add_parsed_decls(env, buffer, directives, decls)
265
+ decls ||= []
266
+ directives ||= []
267
+ if env.respond_to?(:add_source)
268
+ env.add_source(::RBS::Source::RBS.new(buffer, directives, decls))
269
+ elsif env.respond_to?(:add_signature)
270
+ env.add_signature(buffer: buffer, directives: directives, decls: decls)
271
+ else
272
+ decls.each { |decl| env << decl }
273
+ end
274
+ end
275
+
235
276
  # True when a `class_decls` entry was declared in one of the
236
277
  # project's own signature files (by declaration location), so
237
278
  # the sweep skips the bundled stdlib / vendored universe.
238
279
  def project_entry?(entry, project_files)
239
- decl = entry.respond_to?(:primary_decl) ? entry.primary_decl : nil
280
+ decl = primary_decl_for(entry)
240
281
  location = decl&.location
241
282
  buffer_name = location&.buffer&.name
242
283
  return false unless buffer_name
@@ -262,7 +303,7 @@ module Rigor
262
303
  end.join
263
304
  buffer = ::RBS::Buffer.new(name: SYNTHETIC_STUB_BUFFER, content: source)
264
305
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
265
- base_env.add_source(::RBS::Source::RBS.new(buffer, directives || [], decls || []))
306
+ add_parsed_decls(base_env, buffer, directives, decls)
266
307
  rescue ::RBS::BaseError
267
308
  nil
268
309
  end
@@ -284,8 +325,7 @@ module Rigor
284
325
 
285
326
  buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
286
327
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
287
- source = ::RBS::Source::RBS.new(buffer, directives || [], decls || [])
288
- env.add_source(source)
328
+ add_parsed_decls(env, buffer, directives, decls)
289
329
  rescue ::RBS::BaseError
290
330
  # WD6 fail-soft: a single broken virtual RBS contribution
291
331
  # does not pull the whole env down. The plugin layer
@@ -589,7 +629,7 @@ module Rigor
589
629
 
590
630
  result = {}
591
631
  env.class_decls.each do |rbs_name, entry|
592
- decl = entry.primary_decl
632
+ decl = self.class.primary_decl_for(entry)
593
633
  next if decl.nil?
594
634
 
595
635
  location = decl.location
@@ -911,12 +951,17 @@ module Rigor
911
951
  end
912
952
 
913
953
  # Collects the AST declaration nodes behind a `class_decls`
914
- # entry. RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl`;
915
- # the older single-`decl` shape is handled defensively so the
916
- # loader survives an rbs-gem minor bump.
954
+ # entry across the supported RBS range (`rbs >= 3.0, < 5.0`).
955
+ # RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl` yielding
956
+ # bare AST declarations; RBS 3.x exposes `decls`, an array of
957
+ # `MultiEntry::D` wrappers whose `#decl` is the AST declaration.
958
+ # The single-`decl` shape is handled defensively so the loader
959
+ # survives an rbs-gem minor bump.
917
960
  def entry_declarations(entry)
918
961
  if entry.respond_to?(:each_decl)
919
962
  [].tap { |acc| entry.each_decl { |decl| acc << decl } }
963
+ elsif entry.respond_to?(:decls)
964
+ entry.decls.map { |d| d.respond_to?(:decl) ? d.decl : d }
920
965
  elsif entry.respond_to?(:decl)
921
966
  [entry.decl]
922
967
  else
@@ -87,12 +87,27 @@ module Rigor
87
87
  GENUINE_BUG_MAX_COUNT = 5 # rule total ≤ N → "likely genuine bug"
88
88
  private_constant :SYSTEMIC_THRESHOLD, :MONKEY_PATCH_MIN_FILES, :GENUINE_BUG_MAX_COUNT
89
89
 
90
+ # WD6 (ADR-23): H5 (systemic cluster) and H6 (genuine bugs) are
91
+ # the only count-based, severity-agnostic recognisers, so they
92
+ # alone risk reading plugin recognition trace (`:info`
93
+ # `*.model-call` / `*.helper`, …) as a "systemic cluster" or a
94
+ # "genuine bug" — frightening working code. They are guarded
95
+ # against `:info` unless `include_info` restores the pre-v0.2.3
96
+ # behaviour. Every other recogniser keys on an error/warning rule
97
+ # (H1/H2/H2K/H4/H7) or intentionally reads an info notice
98
+ # (H3 — the `gem-without-rbs` `rbs.coverage.missing-gem`), so the
99
+ # full pool is correct for them.
100
+ INFO_GUARDED = %i[h5_systemic_cluster h6_genuine_bugs].freeze
101
+ private_constant :INFO_GUARDED
102
+
90
103
  # @param diagnostics [Array<Analysis::Diagnostic>]
104
+ # @param include_info [Boolean] let H5/H6 see `:info` diagnostics
91
105
  # @return [Array<Hint>]
92
- def recognise(diagnostics)
106
+ def recognise(diagnostics, include_info: false)
93
107
  claimed = {}.compare_by_identity
94
108
  recognisers.filter_map do |recogniser|
95
109
  pool = diagnostics.reject { |d| claimed[d] }
110
+ pool = pool.reject { |d| d.severity == :info } if INFO_GUARDED.include?(recogniser) && !include_info
96
111
  hint, matched = send(recogniser, pool)
97
112
  next unless hint
98
113
 
data/lib/rigor/triage.rb CHANGED
@@ -26,21 +26,40 @@ module Rigor
26
26
  # receiver); `files` is the distinct-file count (a systemic vs.
27
27
  # localised signal); `rules` is the per-rule breakdown.
28
28
  Selector = Data.define(:receiver, :method_name, :count, :files, :rules)
29
- Report = Data.define(:summary, :distribution, :selectors, :hotspots, :hints)
29
+ Report = Data.define(:summary, :distribution, :selectors, :hotspots, :hints, :include_info)
30
30
 
31
31
  module_function
32
32
 
33
+ # WD6 (ADR-23): the volume views — distribution / selectors /
34
+ # hotspots — route only the *actionable* diagnostics (error +
35
+ # warning) by default. Plugin-emitted `:info` diagnostics are
36
+ # overwhelmingly recognition trace (`plugin.activerecord.model-call`,
37
+ # `plugin.rails-routes.helper`, …) — positive "Rigor resolved this
38
+ # call" records, not problems — and on a real Rails app they swamp
39
+ # the genuine error/warning signal (the field trip: 257 of 267
40
+ # diagnostics were such trace) and invert the hotspot ranking
41
+ # towards the files with the *most working* code. The summary still
42
+ # reports the full info count, and `include_info: true` (the
43
+ # `--include-info` flag) restores the pre-v0.2.3 behaviour. Hints
44
+ # always see the full stream so the `gem-without-rbs` notice (an
45
+ # info-severity `rbs.coverage.missing-gem`) survives; the
46
+ # count-based H5/H6 recognisers guard against info themselves so
47
+ # recognition trace never reads as a bug.
48
+ #
33
49
  # @param diagnostics [Array<Analysis::Diagnostic>]
34
50
  # @param top [Integer] hotspot-file cap
35
51
  # @param hints [Boolean] run the heuristic catalogue
52
+ # @param include_info [Boolean] route info into the volume views
36
53
  # @return [Report]
37
- def analyze(diagnostics, top: 10, hints: true)
54
+ def analyze(diagnostics, top: 10, hints: true, include_info: false)
55
+ routed = include_info ? diagnostics : diagnostics.reject { |d| d.severity == :info }
38
56
  Report.new(
39
57
  summary: build_summary(diagnostics),
40
- distribution: build_distribution(diagnostics),
41
- selectors: build_selectors(diagnostics),
42
- hotspots: build_hotspots(diagnostics, top),
43
- hints: hints ? Catalogue.recognise(diagnostics) : []
58
+ distribution: build_distribution(routed),
59
+ selectors: build_selectors(routed),
60
+ hotspots: build_hotspots(routed, top),
61
+ hints: hints ? Catalogue.recognise(diagnostics, include_info: include_info) : [],
62
+ include_info: include_info
44
63
  )
45
64
  end
46
65
 
@@ -150,7 +169,11 @@ module Rigor
150
169
  "hotspots" => report.hotspots.map do |h|
151
170
  { "file" => h.file, "count" => h.count, "by_rule" => h.by_rule }
152
171
  end,
153
- "hints" => report.hints.map(&:to_h)
172
+ "hints" => report.hints.map(&:to_h),
173
+ # WD6: false means distribution / selectors / hotspots above
174
+ # exclude `:info` (their counts will not sum to summary.total);
175
+ # the summary's `info` field still reports the full count.
176
+ "include_info" => report.include_info
154
177
  }
155
178
  end
156
179
  end
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rigortype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rigor contributors