rigortype 0.1.14 → 0.1.16
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 +10 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules.rb +428 -6
- data/lib/rigor/analysis/diagnostic.rb +55 -3
- data/lib/rigor/analysis/rule_catalog.rb +80 -0
- data/lib/rigor/analysis/runner.rb +71 -2
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/cache/descriptor.rb +6 -2
- data/lib/rigor/cli/plugin_command.rb +245 -0
- data/lib/rigor/cli/plugins_command.rb +51 -4
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli.rb +143 -5
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/environment/rbs_loader.rb +259 -1
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/expression_typer.rb +9 -2
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +81 -14
- data/lib/rigor/inference/method_dispatcher.rb +57 -10
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +184 -27
- data/lib/rigor/inference/statement_evaluator.rb +13 -8
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +321 -2
- data/lib/rigor/plugin/box.rb +64 -0
- data/lib/rigor/plugin/inflector.rb +121 -0
- data/lib/rigor/plugin/isolation.rb +191 -0
- data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
- data/lib/rigor/plugin/macro.rb +1 -0
- data/lib/rigor/plugin/manifest.rb +120 -23
- data/lib/rigor/plugin/node_context.rb +62 -0
- data/lib/rigor/plugin/registry.rb +10 -0
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/scope.rb +27 -1
- data/lib/rigor/sig_gen/generator.rb +2 -3
- data/lib/rigor/sig_gen/observation_collector.rb +2 -2
- data/lib/rigor/source/literals.rb +118 -0
- data/lib/rigor/source/node_walker.rb +26 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +71 -5
- data/lib/rigor/type/combinator.rb +6 -1
- data/lib/rigor/type/union.rb +65 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
- data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +36 -31
- 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 +6 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
- data/sig/rigor/plugin/access_denied_error.rbs +3 -1
- data/sig/rigor/plugin/base.rbs +58 -3
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/plugin/manifest.rbs +31 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor.rbs +5 -0
- data/skills/rigor-plugin-author/SKILL.md +33 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +65 -26
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +213 -80
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- data/skills/rigor-project-init/SKILL.md +72 -7
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +233 -19
- metadata +53 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fb4015697adf7cbc9d65b2ca6ff954c8cac5f784ffb9616153d4af8d2505ccc
|
|
4
|
+
data.tar.gz: 52ee5acb7d233c0e86c8cdca45151c5f745b4c13d7155fcb15fafdaaed1d6dc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f4b64afe3dbca26f6224762a2f1a3dd0bd4cade2ea9a61a30e2fba28ed29d3bb6d68bdfa73fb450536e5c7fef45e07a10a6fd0cec693ad228a828ff30905f8e
|
|
7
|
+
data.tar.gz: 4b5faa0d8c7295067f5a0f3d335c1800f22659e47b0428942405b07c1d38649163090f3a461e942a82aecaab6172e774ef88ca44f9a569b1392cdeee0be04086
|
data/README.md
CHANGED
|
@@ -58,6 +58,12 @@ Install Rigor in this project by following the instructions at
|
|
|
58
58
|
https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Prefer to set up in your language? Find prompts for Japanese, Chinese,
|
|
62
|
+
Korean, Portuguese, Spanish, French, German, Italian, Vietnamese, Thai,
|
|
63
|
+
Indonesian, Polish, Ukrainian, Russian, Romanian, and Turkish at
|
|
64
|
+
[docs/manual/14-rails-quickstart.md](docs/manual/14-rails-quickstart.md#step-1--install-ruby-40-and-rigor-common-to-both-paths)
|
|
65
|
+
(or the [online version](https://rigor.typedduck.fail/reference/manual/14-rails-quickstart/)).
|
|
66
|
+
|
|
61
67
|
**Manual install** — the recommended path uses
|
|
62
68
|
[`mise`](https://mise.jdx.dev/), which provisions both Ruby 4.0 and
|
|
63
69
|
Rigor pinned per project:
|
|
@@ -259,10 +265,12 @@ In-source suppression: `# rigor:disable <rule>` silences a single line;
|
|
|
259
265
|
|
|
260
266
|
## Status
|
|
261
267
|
|
|
262
|
-
Current released version: **`v0.1.
|
|
268
|
+
Current released version: **`v0.1.15`** (2026-05-29). The analyzer is
|
|
263
269
|
usable on real Ruby code today; the rule catalogue is deliberately
|
|
264
270
|
conservative — Rigor's stance is to surface zero false positives while
|
|
265
|
-
the inference surface stabilises.
|
|
271
|
+
the inference surface stabilises. The `0.1.x` preview line has been
|
|
272
|
+
hardened against real OSS Rails codebases (Mastodon / Redmine / GitLab
|
|
273
|
+
FOSS); `v0.2.0` will open the first evaluation release.
|
|
266
274
|
|
|
267
275
|
Release history: [`CHANGELOG.md`](CHANGELOG.md). Forward-looking
|
|
268
276
|
commitments: [Roadmap](https://rigor.typedduck.fail/reference/roadmap/).
|
data/exe/rigor
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
# ADR-39 slice 5 — the `ruby_box` plugin-isolation strategy needs the
|
|
5
|
+
# experimental `Ruby::Box` feature, which is a process-start flag
|
|
6
|
+
# (`RUBY_BOX=1`, not toggleable at runtime). When a project selects that
|
|
7
|
+
# strategy (`RIGOR_PLUGIN_ISOLATION=ruby_box`, or the legacy `RIGOR_BOX`
|
|
8
|
+
# alias) we re-exec the same command with the flag set before anything
|
|
9
|
+
# else loads. The `RUBY_BOX` guard prevents an infinite re-exec, and the
|
|
10
|
+
# inherited environment (RUBYOPT / BUNDLE_*) preserves the Bundler
|
|
11
|
+
# context across the exec. The `none` (default) and `process` strategies
|
|
12
|
+
# need no flag, so this is a no-op for them — behaviour is unchanged
|
|
13
|
+
# unless a project opts into `ruby_box`.
|
|
14
|
+
rigor_isolation = ENV["RIGOR_PLUGIN_ISOLATION"].to_s
|
|
15
|
+
rigor_wants_box = rigor_isolation == "ruby_box" || !ENV["RIGOR_BOX"].to_s.empty?
|
|
16
|
+
if rigor_wants_box && ENV["RUBY_BOX"].to_s.empty?
|
|
17
|
+
require "rbconfig"
|
|
18
|
+
ENV["RUBY_BOX"] = "1"
|
|
19
|
+
ENV["RIGOR_PLUGIN_ISOLATION"] = "ruby_box" if rigor_isolation.empty?
|
|
20
|
+
exec(RbConfig.ruby, __FILE__, *ARGV)
|
|
21
|
+
end
|
|
22
|
+
|
|
4
23
|
lib = File.expand_path("../lib", __dir__)
|
|
5
24
|
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
|
6
25
|
|
|
@@ -67,6 +67,9 @@ module Rigor
|
|
|
67
67
|
RULE_UNREACHABLE_BRANCH = "flow.unreachable-branch"
|
|
68
68
|
RULE_RETURN_TYPE = "def.return-type-mismatch"
|
|
69
69
|
RULE_VISIBILITY_MISMATCH = "def.method-visibility-mismatch"
|
|
70
|
+
RULE_OVERRIDE_VISIBILITY_REDUCED = "def.override-visibility-reduced"
|
|
71
|
+
RULE_OVERRIDE_RETURN_WIDENED = "def.override-return-widened"
|
|
72
|
+
RULE_OVERRIDE_PARAM_NARROWED = "def.override-param-narrowed"
|
|
70
73
|
RULE_IVAR_WRITE_MISMATCH = "def.ivar-write-mismatch"
|
|
71
74
|
RULE_DEAD_ASSIGNMENT = "flow.dead-assignment"
|
|
72
75
|
RULE_ALWAYS_TRUTHY_CONDITION = "flow.always-truthy-condition"
|
|
@@ -85,6 +88,9 @@ module Rigor
|
|
|
85
88
|
RULE_ALWAYS_TRUTHY_CONDITION,
|
|
86
89
|
RULE_RETURN_TYPE,
|
|
87
90
|
RULE_VISIBILITY_MISMATCH,
|
|
91
|
+
RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
92
|
+
RULE_OVERRIDE_RETURN_WIDENED,
|
|
93
|
+
RULE_OVERRIDE_PARAM_NARROWED,
|
|
88
94
|
RULE_IVAR_WRITE_MISMATCH
|
|
89
95
|
].freeze
|
|
90
96
|
|
|
@@ -116,6 +122,12 @@ module Rigor
|
|
|
116
122
|
# canonical id starts with `<family>.`. Per ADR-8 § "1".
|
|
117
123
|
RULE_FAMILIES = %w[call flow assert dump def].freeze
|
|
118
124
|
|
|
125
|
+
# ADR-35 slice 1 — bound for the `def.override-visibility-reduced`
|
|
126
|
+
# ancestor walk, and the public > protected > private ordering
|
|
127
|
+
# used to decide whether an override reduces visibility.
|
|
128
|
+
OVERRIDE_ANCESTOR_WALK_LIMIT = 100
|
|
129
|
+
VISIBILITY_RANK = { public: 2, protected: 1, private: 0 }.freeze
|
|
130
|
+
|
|
119
131
|
# Resolves a user-supplied rule token (`undefined-method`,
|
|
120
132
|
# `call.undefined-method`, or the family wildcard `call`)
|
|
121
133
|
# to the set of canonical rule identifiers it disables.
|
|
@@ -150,6 +162,12 @@ module Rigor
|
|
|
150
162
|
when Prism::DefNode
|
|
151
163
|
return_diagnostic = return_type_mismatch_diagnostic(path, node, scope_index)
|
|
152
164
|
diagnostics << return_diagnostic if return_diagnostic
|
|
165
|
+
override_vis = override_visibility_diagnostic(path, node, scope_index)
|
|
166
|
+
diagnostics << override_vis if override_vis
|
|
167
|
+
override_return = override_return_widened_diagnostic(path, node, scope_index)
|
|
168
|
+
diagnostics << override_return if override_return
|
|
169
|
+
override_param = override_param_narrowed_diagnostic(path, node, scope_index)
|
|
170
|
+
diagnostics << override_param if override_param
|
|
153
171
|
when Prism::IfNode, Prism::UnlessNode
|
|
154
172
|
unreachable = unreachable_branch_diagnostic(path, node, scope_index)
|
|
155
173
|
diagnostics << unreachable if unreachable
|
|
@@ -365,7 +383,16 @@ module Rigor
|
|
|
365
383
|
# its model). Flagging an undefined method on a class
|
|
366
384
|
# with an open dynamic surface is unsound, so the rule
|
|
367
385
|
# skips it.
|
|
368
|
-
|
|
386
|
+
# An unbounded receiver surface: either a plugin-declared
|
|
387
|
+
# open receiver (ADR-26 — e.g. `ActiveRecord::Relation`), or
|
|
388
|
+
# a type Rigor synthesized (a missing-namespace module / a
|
|
389
|
+
# stub for a referenced-but-undeclared type) to keep a
|
|
390
|
+
# malformed project signature buildable. A synthesized stub's
|
|
391
|
+
# method table is empty only because Rigor invented it, not
|
|
392
|
+
# because the real type is empty (the real `DRb` has
|
|
393
|
+
# `start_service`), so enumerating it to prove a call
|
|
394
|
+
# "undefined" would be a false positive.
|
|
395
|
+
return nil if unbounded_receiver_surface?(class_name, scope)
|
|
369
396
|
|
|
370
397
|
# Slice 7 phase 12 — suppress when the user has
|
|
371
398
|
# declared the method in source (`def` /
|
|
@@ -401,7 +428,24 @@ module Rigor
|
|
|
401
428
|
return nil if module_mixin_receiver?(receiver_type, scope) &&
|
|
402
429
|
lookup_method(receiver_type, "Object", call_node.name, scope)
|
|
403
430
|
|
|
404
|
-
|
|
431
|
+
definition_site = project_definition_site(scope, class_name, call_node.name, kind)
|
|
432
|
+
build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site, class_name)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# ADR-17 — when the project itself defines this method on the
|
|
436
|
+
# receiver class somewhere in the analyzed file set (a reopened
|
|
437
|
+
# core/stdlib/gem class the dispatcher does not apply cross-
|
|
438
|
+
# file), return that `"path:line"` site so the diagnostic points
|
|
439
|
+
# at `pre_eval:` instead of reading as a bare unresolved call.
|
|
440
|
+
# Instance-side only (the cross-file def-source index tracks
|
|
441
|
+
# `def` instance methods); the diagnostic still fires — Rigor
|
|
442
|
+
# does not auto-apply project monkey-patches (the full-project
|
|
443
|
+
# pre-pass is deferred per ADR-17 slice 5) — but it is now
|
|
444
|
+
# actionable rather than mistakable for a typo.
|
|
445
|
+
def project_definition_site(scope, class_name, method_name, kind)
|
|
446
|
+
return nil unless kind == :instance
|
|
447
|
+
|
|
448
|
+
scope.user_def_site_for(class_name, method_name)
|
|
405
449
|
end
|
|
406
450
|
|
|
407
451
|
def module_mixin_receiver?(receiver_type, scope)
|
|
@@ -493,7 +537,8 @@ module Rigor
|
|
|
493
537
|
"`def` or a monkey-patch on Object/Kernel, list that file in " \
|
|
494
538
|
"`.rigor.yml`'s `pre_eval:` (ADR-17) so the analyzer sees it.",
|
|
495
539
|
severity: :warning,
|
|
496
|
-
rule: RULE_UNRESOLVED_TOPLEVEL
|
|
540
|
+
rule: RULE_UNRESOLVED_TOPLEVEL,
|
|
541
|
+
method_name: call_node.name.to_s
|
|
497
542
|
)
|
|
498
543
|
end
|
|
499
544
|
|
|
@@ -530,6 +575,14 @@ module Rigor
|
|
|
530
575
|
# loaded plugin (manifest `open_receivers:`). An open
|
|
531
576
|
# class responds beyond its RBS surface, so the
|
|
532
577
|
# `call.undefined-method` rule must not fire for it.
|
|
578
|
+
# True when the receiver class responds beyond an enumerable
|
|
579
|
+
# RBS method table, so proving a call "undefined" against it is
|
|
580
|
+
# unsound: a plugin-declared open receiver, or a Rigor-
|
|
581
|
+
# synthesized stub type (see `RbsLoader#synthesized_type_names`).
|
|
582
|
+
def unbounded_receiver_surface?(class_name, scope)
|
|
583
|
+
open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope)
|
|
584
|
+
end
|
|
585
|
+
|
|
533
586
|
def open_receiver?(class_name, scope)
|
|
534
587
|
registry = scope.environment&.plugin_registry
|
|
535
588
|
return false if registry.nil?
|
|
@@ -537,6 +590,13 @@ module Rigor
|
|
|
537
590
|
registry.open_receiver?(class_name)
|
|
538
591
|
end
|
|
539
592
|
|
|
593
|
+
def synthesized_stub_receiver?(class_name, scope)
|
|
594
|
+
loader = scope.environment&.rbs_loader
|
|
595
|
+
return false if loader.nil? || !loader.respond_to?(:synthesized_type_names)
|
|
596
|
+
|
|
597
|
+
loader.synthesized_type_names.include?(class_name.to_s.sub(/\A::/, ""))
|
|
598
|
+
end
|
|
599
|
+
|
|
540
600
|
def definition_available?(receiver_type, class_name, scope)
|
|
541
601
|
if receiver_type.is_a?(Type::Singleton)
|
|
542
602
|
!Rigor::Reflection.singleton_definition(class_name, scope: scope).nil?
|
|
@@ -1319,18 +1379,33 @@ module Rigor
|
|
|
1319
1379
|
)
|
|
1320
1380
|
end
|
|
1321
1381
|
|
|
1322
|
-
def build_undefined_method_diagnostic(path, call_node, receiver_type)
|
|
1382
|
+
def build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site = nil, class_name = nil)
|
|
1323
1383
|
location = call_node.message_loc || call_node.location
|
|
1324
1384
|
rendered_receiver = receiver_type.describe
|
|
1385
|
+
message = "undefined method `#{call_node.name}' for #{rendered_receiver}"
|
|
1386
|
+
# ADR-17 — when the project itself defines this method on the
|
|
1387
|
+
# receiver class somewhere in the file set, name the site and
|
|
1388
|
+
# point at `pre_eval:`. Rigor does not apply project monkey-
|
|
1389
|
+
# patches cross-file automatically, so the diagnostic still
|
|
1390
|
+
# fires, but the enriched message makes it actionable (and
|
|
1391
|
+
# `rigor triage` keys on the structured `project_definition_site`
|
|
1392
|
+
# field to recommend `pre_eval:` with high confidence).
|
|
1393
|
+
if definition_site
|
|
1394
|
+
def_owner = class_name || rendered_receiver
|
|
1395
|
+
message += "; the project defines `#{def_owner}##{call_node.name}' at " \
|
|
1396
|
+
"#{definition_site} — Rigor does not apply project monkey-patches " \
|
|
1397
|
+
"cross-file; list that file in `.rigor.yml`'s `pre_eval:` (ADR-17)"
|
|
1398
|
+
end
|
|
1325
1399
|
Diagnostic.new(
|
|
1326
1400
|
rule: RULE_UNDEFINED_METHOD,
|
|
1327
1401
|
path: path,
|
|
1328
1402
|
line: location.start_line,
|
|
1329
1403
|
column: location.start_column + 1,
|
|
1330
|
-
message:
|
|
1404
|
+
message: message,
|
|
1331
1405
|
severity: :error,
|
|
1332
1406
|
receiver_type: rendered_receiver,
|
|
1333
|
-
method_name: call_node.name.to_s
|
|
1407
|
+
method_name: call_node.name.to_s,
|
|
1408
|
+
project_definition_site: definition_site
|
|
1334
1409
|
)
|
|
1335
1410
|
end
|
|
1336
1411
|
|
|
@@ -1480,6 +1555,353 @@ module Rigor
|
|
|
1480
1555
|
severity: severity
|
|
1481
1556
|
)
|
|
1482
1557
|
end
|
|
1558
|
+
|
|
1559
|
+
# ADR-35 slice 1 — `def.override-visibility-reduced`. The
|
|
1560
|
+
# Liskov signature rule for visibility: an instance-method
|
|
1561
|
+
# override MUST NOT reduce the visibility it inherits
|
|
1562
|
+
# (public → protected/private, or protected → private),
|
|
1563
|
+
# because a caller holding the supertype that invokes the
|
|
1564
|
+
# method breaks when handed the subtype.
|
|
1565
|
+
#
|
|
1566
|
+
# Slice-1 scope (ADR-35 WD1, visibility carve-out): both the
|
|
1567
|
+
# override and the shadowed method must have a STATICALLY
|
|
1568
|
+
# OBSERVABLE visibility. The override's visibility is read
|
|
1569
|
+
# from the source-discovered table; the parent is resolved
|
|
1570
|
+
# against the project-discovered ancestor chain (user-source
|
|
1571
|
+
# classes / modules only — RBS-known ancestors, whose
|
|
1572
|
+
# accessibility RBS models as public/private only, are a
|
|
1573
|
+
# deferred follow-on). When either side is not observable
|
|
1574
|
+
# the rule stays silent.
|
|
1575
|
+
def override_visibility_diagnostic(path, def_node, scope_index)
|
|
1576
|
+
return nil unless def_node.receiver.nil? # instance methods only
|
|
1577
|
+
|
|
1578
|
+
scope = scope_index[def_node]
|
|
1579
|
+
return nil if scope.nil?
|
|
1580
|
+
|
|
1581
|
+
self_type = scope.self_type
|
|
1582
|
+
return nil unless self_type.respond_to?(:class_name)
|
|
1583
|
+
|
|
1584
|
+
class_name = self_type.class_name.to_s
|
|
1585
|
+
method_name = def_node.name
|
|
1586
|
+
|
|
1587
|
+
override_visibility = scope.discovered_method_visibility(class_name, method_name)
|
|
1588
|
+
return nil if override_visibility.nil?
|
|
1589
|
+
|
|
1590
|
+
parent = nearest_ancestor_visibility(scope, class_name, method_name)
|
|
1591
|
+
return nil if parent.nil?
|
|
1592
|
+
|
|
1593
|
+
parent_class, parent_visibility = parent
|
|
1594
|
+
# Unknown ancestor visibility (e.g. the defining file was not
|
|
1595
|
+
# in the analyzed set) → cannot prove a reduction, stay silent.
|
|
1596
|
+
return nil if parent_visibility.nil?
|
|
1597
|
+
return nil unless visibility_reduced?(parent_visibility, override_visibility)
|
|
1598
|
+
|
|
1599
|
+
build_override_visibility_diagnostic(
|
|
1600
|
+
path, def_node, parent_class, parent_visibility, override_visibility
|
|
1601
|
+
)
|
|
1602
|
+
end
|
|
1603
|
+
|
|
1604
|
+
# Returns true when `override_visibility` is strictly more
|
|
1605
|
+
# restrictive than `parent_visibility` under the
|
|
1606
|
+
# public > protected > private ordering.
|
|
1607
|
+
def visibility_reduced?(parent_visibility, override_visibility)
|
|
1608
|
+
parent_rank = VISIBILITY_RANK[parent_visibility]
|
|
1609
|
+
override_rank = VISIBILITY_RANK[override_visibility]
|
|
1610
|
+
return false if parent_rank.nil? || override_rank.nil?
|
|
1611
|
+
|
|
1612
|
+
override_rank < parent_rank
|
|
1613
|
+
end
|
|
1614
|
+
|
|
1615
|
+
# Breadth-first walk of the project-discovered ancestor chain
|
|
1616
|
+
# (included / prepended modules first, then the superclass —
|
|
1617
|
+
# Ruby's MRO ordering), yielding each resolved ancestor class
|
|
1618
|
+
# name nearest-first. Returns the first truthy value the block
|
|
1619
|
+
# produces, or nil. Cross-file: the chain is followed through
|
|
1620
|
+
# the scope tables the runner seeds from the project pre-pass
|
|
1621
|
+
# (ADR-24 WD1). Cycle-guarded and node-count-capped. Mirrors
|
|
1622
|
+
# `ExpressionTyper#resolve_user_def_through_ancestors`.
|
|
1623
|
+
def each_project_ancestor(scope, class_name)
|
|
1624
|
+
queue = ancestor_class_names(scope, class_name)
|
|
1625
|
+
seen = { class_name.to_s => true }
|
|
1626
|
+
visited = 0
|
|
1627
|
+
until queue.empty?
|
|
1628
|
+
current = queue.shift
|
|
1629
|
+
next if current.nil? || seen[current]
|
|
1630
|
+
|
|
1631
|
+
seen[current] = true
|
|
1632
|
+
visited += 1
|
|
1633
|
+
return nil if visited > OVERRIDE_ANCESTOR_WALK_LIMIT
|
|
1634
|
+
|
|
1635
|
+
result = yield current
|
|
1636
|
+
return result if result
|
|
1637
|
+
|
|
1638
|
+
ancestor_class_names(scope, current).each { |name| queue.push(name) }
|
|
1639
|
+
end
|
|
1640
|
+
nil
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
# `[defining_class, visibility]` for the nearest user-source
|
|
1644
|
+
# ancestor that defines an instance method `method_name`, or nil.
|
|
1645
|
+
def nearest_ancestor_visibility(scope, class_name, method_name)
|
|
1646
|
+
each_project_ancestor(scope, class_name) do |ancestor|
|
|
1647
|
+
# Stop at the nearest ancestor that DEFINES the method; its
|
|
1648
|
+
# visibility may be nil (unknown) — the caller treats unknown
|
|
1649
|
+
# as "cannot prove a reduction" and stays silent. Never
|
|
1650
|
+
# fabricate `:public` from a missing entry (that produced a
|
|
1651
|
+
# large false-positive cluster on cross-file Rails concerns).
|
|
1652
|
+
[ancestor, scope.discovered_method_visibility(ancestor, method_name)] if scope.user_def_for(ancestor,
|
|
1653
|
+
method_name)
|
|
1654
|
+
end
|
|
1655
|
+
end
|
|
1656
|
+
|
|
1657
|
+
# Direct ancestors of `class_name` as project-discovered,
|
|
1658
|
+
# qualified names: included / prepended modules first, then
|
|
1659
|
+
# the superclass. As-written names are resolved against the
|
|
1660
|
+
# subclass's lexical nesting; names that resolve to no
|
|
1661
|
+
# project class/module (RBS-known / third-party) are dropped.
|
|
1662
|
+
def ancestor_class_names(scope, class_name)
|
|
1663
|
+
names = []
|
|
1664
|
+
scope.includes_of(class_name).each do |raw|
|
|
1665
|
+
resolved = resolve_override_ancestor_name(scope, class_name, raw)
|
|
1666
|
+
names << resolved if resolved
|
|
1667
|
+
end
|
|
1668
|
+
raw_super = scope.superclass_of(class_name)
|
|
1669
|
+
if raw_super
|
|
1670
|
+
resolved_super = resolve_override_ancestor_name(scope, class_name, raw_super)
|
|
1671
|
+
names << resolved_super if resolved_super
|
|
1672
|
+
end
|
|
1673
|
+
names
|
|
1674
|
+
end
|
|
1675
|
+
|
|
1676
|
+
def resolve_override_ancestor_name(scope, subclass_qualified, raw_ancestor)
|
|
1677
|
+
segments = subclass_qualified.to_s.split("::")
|
|
1678
|
+
(segments.length - 1).downto(0) do |i|
|
|
1679
|
+
candidate = (segments[0, i] + [raw_ancestor]).join("::")
|
|
1680
|
+
return candidate if known_user_class?(scope, candidate)
|
|
1681
|
+
end
|
|
1682
|
+
nil
|
|
1683
|
+
end
|
|
1684
|
+
|
|
1685
|
+
def known_user_class?(scope, name)
|
|
1686
|
+
scope.discovered_superclasses.key?(name) ||
|
|
1687
|
+
scope.discovered_def_nodes.key?(name) ||
|
|
1688
|
+
scope.discovered_includes.key?(name)
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
def build_override_visibility_diagnostic(path, def_node, parent_class, parent_visibility, override_visibility)
|
|
1692
|
+
location = def_node.name_loc || def_node.location
|
|
1693
|
+
Diagnostic.new(
|
|
1694
|
+
rule: RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
1695
|
+
path: path,
|
|
1696
|
+
line: location.start_line,
|
|
1697
|
+
column: location.start_column + 1,
|
|
1698
|
+
message: "visibility of `#{def_node.name}' reduced from #{parent_visibility} to " \
|
|
1699
|
+
"#{override_visibility} (overrides #{parent_class}##{def_node.name}); " \
|
|
1700
|
+
"breaks substitutability",
|
|
1701
|
+
severity: :warning
|
|
1702
|
+
)
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
# ADR-35 slice 2 — `def.override-return-widened`. The Liskov
|
|
1706
|
+
# signature rule for returns (covariance): an override may
|
|
1707
|
+
# *narrow* the return it inherits (return a more specific type)
|
|
1708
|
+
# but MUST NOT *widen* it. A caller holding the supertype uses
|
|
1709
|
+
# the result as the parent's return type; a wider override
|
|
1710
|
+
# return breaks that use.
|
|
1711
|
+
#
|
|
1712
|
+
# WD1 gate (proper, type-direction): both the override and the
|
|
1713
|
+
# shadowed ancestor method must carry an explicitly-authored
|
|
1714
|
+
# RBS signature. The override side is gated by
|
|
1715
|
+
# `defined_on?` (the RBS method is declared on the overriding
|
|
1716
|
+
# class itself, not merely inherited); the parent side is the
|
|
1717
|
+
# nearest project-discovered ancestor whose RBS declares the
|
|
1718
|
+
# method. Inference-only either side → silent.
|
|
1719
|
+
#
|
|
1720
|
+
# Fires only on a proven (`:no`) widening; generic / `untyped`
|
|
1721
|
+
# / `self` parent returns degrade to `Dynamic[Top]` and accept
|
|
1722
|
+
# everything, so they stay silent (FP-safe). `self`/`instance`
|
|
1723
|
+
# are translated with `self_type: nil` on both sides, so a
|
|
1724
|
+
# parent `-> self` and an override `-> self` never fire.
|
|
1725
|
+
def override_return_widened_diagnostic(path, def_node, scope_index)
|
|
1726
|
+
return nil unless def_node.receiver.nil? # instance methods only (singleton: follow-on)
|
|
1727
|
+
|
|
1728
|
+
scope = scope_index[def_node]
|
|
1729
|
+
return nil if scope.nil?
|
|
1730
|
+
|
|
1731
|
+
self_type = scope.self_type
|
|
1732
|
+
return nil unless self_type.respond_to?(:class_name)
|
|
1733
|
+
|
|
1734
|
+
class_name = self_type.class_name.to_s
|
|
1735
|
+
method_name = def_node.name
|
|
1736
|
+
|
|
1737
|
+
override_method = safe_instance_method_definition(class_name, method_name, scope)
|
|
1738
|
+
return nil if override_method.nil?
|
|
1739
|
+
return nil unless defined_on?(override_method, class_name)
|
|
1740
|
+
|
|
1741
|
+
parent = nearest_ancestor_method_def(scope, class_name, method_name)
|
|
1742
|
+
return nil if parent.nil?
|
|
1743
|
+
|
|
1744
|
+
parent_class, parent_method = parent
|
|
1745
|
+
override_return = declared_return_union(override_method, scope.environment)
|
|
1746
|
+
parent_return = declared_return_union(parent_method, scope.environment)
|
|
1747
|
+
return nil if override_return.nil? || parent_return.nil?
|
|
1748
|
+
return nil if dynamic_top?(parent_return) # untyped / unbound-generic parent contract
|
|
1749
|
+
|
|
1750
|
+
return nil unless parent_return.accepts(override_return).no?
|
|
1751
|
+
|
|
1752
|
+
build_override_return_widened_diagnostic(
|
|
1753
|
+
path, def_node, parent_class, parent_return, override_return
|
|
1754
|
+
)
|
|
1755
|
+
end
|
|
1756
|
+
|
|
1757
|
+
# `[defining_class, RBS::Definition::Method]` for the nearest
|
|
1758
|
+
# project-discovered ancestor whose RBS declares `method_name`
|
|
1759
|
+
# (not the starting class's own declaration), or nil.
|
|
1760
|
+
def nearest_ancestor_method_def(scope, class_name, method_name)
|
|
1761
|
+
each_project_ancestor(scope, class_name) do |ancestor|
|
|
1762
|
+
method_def = safe_instance_method_definition(ancestor, method_name, scope)
|
|
1763
|
+
[ancestor, method_def] if method_def && !defined_on?(method_def, class_name)
|
|
1764
|
+
end
|
|
1765
|
+
end
|
|
1766
|
+
|
|
1767
|
+
def safe_instance_method_definition(class_name, method_name, scope)
|
|
1768
|
+
Reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
1769
|
+
rescue StandardError
|
|
1770
|
+
nil
|
|
1771
|
+
end
|
|
1772
|
+
|
|
1773
|
+
# True when `method_def`'s RBS declaration lives on `class_name`
|
|
1774
|
+
# itself (rather than being inherited from an ancestor).
|
|
1775
|
+
def defined_on?(method_def, class_name)
|
|
1776
|
+
defined_in = method_def.defined_in
|
|
1777
|
+
return false if defined_in.nil?
|
|
1778
|
+
|
|
1779
|
+
normalize_class_name(defined_in.to_s) == normalize_class_name(class_name)
|
|
1780
|
+
end
|
|
1781
|
+
|
|
1782
|
+
def normalize_class_name(name)
|
|
1783
|
+
name.to_s.delete_prefix("::")
|
|
1784
|
+
end
|
|
1785
|
+
|
|
1786
|
+
def build_override_return_widened_diagnostic(path, def_node, parent_class, parent_return, override_return)
|
|
1787
|
+
location = def_node.name_loc || def_node.location
|
|
1788
|
+
Diagnostic.new(
|
|
1789
|
+
rule: RULE_OVERRIDE_RETURN_WIDENED,
|
|
1790
|
+
path: path,
|
|
1791
|
+
line: location.start_line,
|
|
1792
|
+
column: location.start_column + 1,
|
|
1793
|
+
message: "return type of `#{def_node.name}' widened from #{parent_return.describe(:short)} " \
|
|
1794
|
+
"to #{override_return.describe(:short)} (overrides #{parent_class}##{def_node.name}); " \
|
|
1795
|
+
"breaks substitutability",
|
|
1796
|
+
severity: :warning
|
|
1797
|
+
)
|
|
1798
|
+
end
|
|
1799
|
+
|
|
1800
|
+
# ADR-35 slice 3 — `def.override-param-narrowed`. The Liskov
|
|
1801
|
+
# signature rule for parameters (contravariance): an override
|
|
1802
|
+
# may *widen* a parameter (accept a supertype — accepting more
|
|
1803
|
+
# is safe) but MUST NOT *narrow* it. A caller holding the
|
|
1804
|
+
# supertype passes a parent-typed argument; a narrowed override
|
|
1805
|
+
# parameter cannot accept it.
|
|
1806
|
+
#
|
|
1807
|
+
# Direction (ADR-35 WD3, corrected): fire on
|
|
1808
|
+
# `override_param.accepts(parent_param) == :no` — the override's
|
|
1809
|
+
# (narrowed) slot cannot accept the wider parent argument type.
|
|
1810
|
+
# WD4: type comparison at matching POSITIONAL parameter indices
|
|
1811
|
+
# only; arity / keyword-requiredness divergence is out of scope
|
|
1812
|
+
# for v1. Same WD1 both-sides-authored gate as slice 2;
|
|
1813
|
+
# `untyped` / unbound-generic / interface parent params degrade
|
|
1814
|
+
# to `Dynamic[Top]` and are skipped (FP-safe). To avoid
|
|
1815
|
+
# overload-arm ambiguity, both sides must have exactly one
|
|
1816
|
+
# method type.
|
|
1817
|
+
def override_param_narrowed_diagnostic(path, def_node, scope_index)
|
|
1818
|
+
return nil unless def_node.receiver.nil? # instance methods only
|
|
1819
|
+
|
|
1820
|
+
scope = scope_index[def_node]
|
|
1821
|
+
return nil if scope.nil?
|
|
1822
|
+
|
|
1823
|
+
self_type = scope.self_type
|
|
1824
|
+
return nil unless self_type.respond_to?(:class_name)
|
|
1825
|
+
|
|
1826
|
+
class_name = self_type.class_name.to_s
|
|
1827
|
+
method_name = def_node.name
|
|
1828
|
+
|
|
1829
|
+
override_method = safe_instance_method_definition(class_name, method_name, scope)
|
|
1830
|
+
return nil if override_method.nil?
|
|
1831
|
+
return nil unless defined_on?(override_method, class_name)
|
|
1832
|
+
|
|
1833
|
+
parent = nearest_ancestor_method_def(scope, class_name, method_name)
|
|
1834
|
+
return nil if parent.nil?
|
|
1835
|
+
|
|
1836
|
+
parent_class, parent_method = parent
|
|
1837
|
+
override_params = positional_param_types(override_method)
|
|
1838
|
+
parent_params = positional_param_types(parent_method)
|
|
1839
|
+
return nil if override_params.nil? || parent_params.nil?
|
|
1840
|
+
|
|
1841
|
+
index = first_narrowed_param_index(override_params, parent_params)
|
|
1842
|
+
return nil if index.nil?
|
|
1843
|
+
|
|
1844
|
+
build_override_param_narrowed_diagnostic(
|
|
1845
|
+
path, def_node, parent_class, index, parent_params[index], override_params[index]
|
|
1846
|
+
)
|
|
1847
|
+
end
|
|
1848
|
+
|
|
1849
|
+
# Translated positional (required + optional) parameter types of
|
|
1850
|
+
# a method's single method type, or nil when the method is
|
|
1851
|
+
# overloaded (multiple method types — arm mapping is ambiguous)
|
|
1852
|
+
# or the parameter list is not introspectable. Per-position
|
|
1853
|
+
# translation failures yield `nil` at that slot (skipped by the
|
|
1854
|
+
# comparison). `self`/`instance` translate with `self_type: nil`
|
|
1855
|
+
# (→ `Dynamic[Top]`), matching the return-side handling.
|
|
1856
|
+
def positional_param_types(method_def)
|
|
1857
|
+
method_types = method_def.method_types
|
|
1858
|
+
return nil unless method_types.size == 1
|
|
1859
|
+
|
|
1860
|
+
func = method_types.first.type
|
|
1861
|
+
return nil unless func.respond_to?(:required_positionals)
|
|
1862
|
+
|
|
1863
|
+
(func.required_positionals + func.optional_positionals).map do |param|
|
|
1864
|
+
Inference::RbsTypeTranslator.translate(
|
|
1865
|
+
param.type, self_type: nil, instance_type: nil, type_vars: {}
|
|
1866
|
+
)
|
|
1867
|
+
rescue StandardError
|
|
1868
|
+
nil
|
|
1869
|
+
end
|
|
1870
|
+
end
|
|
1871
|
+
|
|
1872
|
+
# Index of the first positional parameter the override narrows
|
|
1873
|
+
# relative to the parent, or nil. A position is a violation when
|
|
1874
|
+
# the override's slot cannot accept the parent's argument type
|
|
1875
|
+
# (`override_param.accepts(parent_param) == :no`). Positions
|
|
1876
|
+
# where either side is missing/untranslatable, or the parent
|
|
1877
|
+
# type degraded to `Dynamic[Top]` (untyped / unbound generic /
|
|
1878
|
+
# interface), are skipped.
|
|
1879
|
+
def first_narrowed_param_index(override_params, parent_params)
|
|
1880
|
+
count = [override_params.size, parent_params.size].min
|
|
1881
|
+
count.times do |i|
|
|
1882
|
+
override_param = override_params[i]
|
|
1883
|
+
parent_param = parent_params[i]
|
|
1884
|
+
next if override_param.nil? || parent_param.nil?
|
|
1885
|
+
next if dynamic_top?(parent_param) || dynamic_top?(override_param)
|
|
1886
|
+
|
|
1887
|
+
return i if override_param.accepts(parent_param).no?
|
|
1888
|
+
end
|
|
1889
|
+
nil
|
|
1890
|
+
end
|
|
1891
|
+
|
|
1892
|
+
def build_override_param_narrowed_diagnostic(path, def_node, parent_class, index, parent_param, override_param)
|
|
1893
|
+
location = def_node.name_loc || def_node.location
|
|
1894
|
+
Diagnostic.new(
|
|
1895
|
+
rule: RULE_OVERRIDE_PARAM_NARROWED,
|
|
1896
|
+
path: path,
|
|
1897
|
+
line: location.start_line,
|
|
1898
|
+
column: location.start_column + 1,
|
|
1899
|
+
message: "parameter #{index + 1} of `#{def_node.name}' narrowed from " \
|
|
1900
|
+
"#{parent_param.describe(:short)} to #{override_param.describe(:short)} " \
|
|
1901
|
+
"(overrides #{parent_class}##{def_node.name}); breaks substitutability",
|
|
1902
|
+
severity: :warning
|
|
1903
|
+
)
|
|
1904
|
+
end
|
|
1483
1905
|
end
|
|
1484
1906
|
# rubocop:enable Metrics/ClassLength
|
|
1485
1907
|
end
|
|
@@ -9,7 +9,7 @@ module Rigor
|
|
|
9
9
|
DEFAULT_SOURCE_FAMILY = :builtin
|
|
10
10
|
|
|
11
11
|
attr_reader :path, :line, :column, :message, :severity, :rule, :source_family,
|
|
12
|
-
:receiver_type, :method_name
|
|
12
|
+
:receiver_type, :method_name, :project_definition_site
|
|
13
13
|
|
|
14
14
|
# `rule:` is the stable identifier (a kebab-case string)
|
|
15
15
|
# of the diagnostic's source rule. It is used by the
|
|
@@ -35,9 +35,18 @@ module Rigor
|
|
|
35
35
|
# message wording. Both stay nil for rules that have no such
|
|
36
36
|
# pair; a consumer that finds them nil falls back to message
|
|
37
37
|
# parsing.
|
|
38
|
+
#
|
|
39
|
+
# `project_definition_site:` is an optional `"path:line"` string
|
|
40
|
+
# set by `call.undefined-method` when the project itself defines
|
|
41
|
+
# the called method on the receiver class somewhere in the
|
|
42
|
+
# analyzed file set (a reopened core/stdlib/gem class the
|
|
43
|
+
# dispatcher does not apply cross-file — see ADR-17). Its presence
|
|
44
|
+
# is the high-confidence "this is a project monkey-patch, not a
|
|
45
|
+
# bug" signal `rigor triage` keys on to recommend `pre_eval:`.
|
|
46
|
+
# Nil for every other diagnostic.
|
|
38
47
|
def initialize(path:, line:, column:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
|
|
39
48
|
source_family: DEFAULT_SOURCE_FAMILY,
|
|
40
|
-
receiver_type: nil, method_name: nil)
|
|
49
|
+
receiver_type: nil, method_name: nil, project_definition_site: nil)
|
|
41
50
|
@path = path
|
|
42
51
|
@line = line
|
|
43
52
|
@column = column
|
|
@@ -47,6 +56,47 @@ module Rigor
|
|
|
47
56
|
@source_family = source_family
|
|
48
57
|
@receiver_type = receiver_type
|
|
49
58
|
@method_name = method_name
|
|
59
|
+
@project_definition_site = project_definition_site
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Builds a Diagnostic positioned at a Prism node. Internalises
|
|
63
|
+
# the load-bearing convention every caller otherwise repeats:
|
|
64
|
+
# the line is the node's 1-based `start_line` and the column is
|
|
65
|
+
# `start_column + 1` (Prism columns are 0-based; Rigor reports
|
|
66
|
+
# 1-based). Pass any node responding to `#location`; all other
|
|
67
|
+
# fields forward to `#initialize` unchanged.
|
|
68
|
+
#
|
|
69
|
+
# `Plugin::Base#diagnostic` wraps this for plugin authors (who
|
|
70
|
+
# must not set `source_family` — the runner stamps it); core
|
|
71
|
+
# rules and other producers call it directly.
|
|
72
|
+
def self.from_node(node, path:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
|
|
73
|
+
source_family: DEFAULT_SOURCE_FAMILY,
|
|
74
|
+
receiver_type: nil, method_name: nil, project_definition_site: nil)
|
|
75
|
+
from_location(
|
|
76
|
+
node.location, path: path, message: message, severity: severity, rule: rule,
|
|
77
|
+
source_family: source_family, receiver_type: receiver_type,
|
|
78
|
+
method_name: method_name, project_definition_site: project_definition_site
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Builds a Diagnostic from an explicit Prism location, applying
|
|
83
|
+
# the same 1-based `line` / `start_column + 1` convention as
|
|
84
|
+
# {.from_node}. Use this when the diagnostic should point at a
|
|
85
|
+
# *sub-location* rather than the whole node — most often a call's
|
|
86
|
+
# `message_loc` (the matcher / method name) instead of the
|
|
87
|
+
# receiver-spanning `node.location`. {.from_node} is sugar for
|
|
88
|
+
# `from_location(node.location, …)`.
|
|
89
|
+
def self.from_location(location, path:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
|
|
90
|
+
source_family: DEFAULT_SOURCE_FAMILY,
|
|
91
|
+
receiver_type: nil, method_name: nil, project_definition_site: nil)
|
|
92
|
+
new(
|
|
93
|
+
path: path,
|
|
94
|
+
line: location.start_line,
|
|
95
|
+
column: location.start_column + 1,
|
|
96
|
+
message: message, severity: severity, rule: rule, source_family: source_family,
|
|
97
|
+
receiver_type: receiver_type, method_name: method_name,
|
|
98
|
+
project_definition_site: project_definition_site
|
|
99
|
+
)
|
|
50
100
|
end
|
|
51
101
|
|
|
52
102
|
def error?
|
|
@@ -65,7 +115,7 @@ module Rigor
|
|
|
65
115
|
end
|
|
66
116
|
|
|
67
117
|
def to_h
|
|
68
|
-
{
|
|
118
|
+
base = {
|
|
69
119
|
"path" => path,
|
|
70
120
|
"line" => line,
|
|
71
121
|
"column" => column,
|
|
@@ -74,6 +124,8 @@ module Rigor
|
|
|
74
124
|
"source_family" => source_family.to_s,
|
|
75
125
|
"message" => message
|
|
76
126
|
}
|
|
127
|
+
base["project_definition_site"] = project_definition_site if project_definition_site
|
|
128
|
+
base
|
|
77
129
|
end
|
|
78
130
|
|
|
79
131
|
# Text rendering for `rigor check`. The qualified rule
|