rigortype 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
- data/lib/rigor/analysis/check_rules.rb +174 -71
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +58 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner.rb +485 -29
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +56 -51
- data/lib/rigor/cache/incremental_snapshot.rb +147 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +99 -1
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +52 -10
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +260 -48
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +13 -3
- data/lib/rigor/environment/rbs_loader.rb +335 -4
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +149 -22
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +100 -23
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +147 -60
- data/lib/rigor/inference/narrowing.rb +202 -5
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +257 -11
- data/lib/rigor/inference/statement_evaluator.rb +110 -26
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +337 -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 +49 -1
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope.rb +123 -9
- 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/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +23 -1
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +69 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -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 +48 -33
- 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/cache.rbs +19 -0
- data/sig/rigor/inference.rbs +22 -0
- 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/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +5 -0
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +11 -1
- data/skills/rigor-plugin-author/SKILL.md +13 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- metadata +73 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "prism"
|
|
4
5
|
require "tmpdir"
|
|
5
6
|
|
|
6
7
|
require_relative "../environment"
|
|
7
8
|
require_relative "../scope"
|
|
8
9
|
require_relative "../cache/store"
|
|
10
|
+
require_relative "../cache/rbs_descriptor"
|
|
9
11
|
require_relative "../plugin"
|
|
10
12
|
require_relative "../plugin/source_rbs_synthesis_reporter"
|
|
11
13
|
require_relative "../rbs_extended/reporter"
|
|
14
|
+
require_relative "../rbs_extended/conformance_checker"
|
|
12
15
|
require_relative "../reflection"
|
|
13
16
|
require_relative "../type/combinator"
|
|
14
17
|
require_relative "../inference/coverage_scanner"
|
|
@@ -18,6 +21,10 @@ require_relative "../inference/project_patched_scanner"
|
|
|
18
21
|
require_relative "../inference/method_dispatcher/file_folding"
|
|
19
22
|
require_relative "buffer_binding"
|
|
20
23
|
require_relative "check_rules"
|
|
24
|
+
require_relative "dependency_recorder"
|
|
25
|
+
require_relative "self_call_resolution_recorder"
|
|
26
|
+
require_relative "incremental"
|
|
27
|
+
require_relative "incremental_session"
|
|
21
28
|
require_relative "dependency_source_inference"
|
|
22
29
|
require_relative "diagnostic"
|
|
23
30
|
require_relative "erb_template_detector"
|
|
@@ -33,7 +40,8 @@ module Rigor
|
|
|
33
40
|
DEFAULT_CACHE_ROOT = ".rigor/cache"
|
|
34
41
|
|
|
35
42
|
attr_reader :cache_store, :plugin_registry, :dependency_source_index,
|
|
36
|
-
:rbs_extended_reporter, :boundary_cross_reporter
|
|
43
|
+
:rbs_extended_reporter, :boundary_cross_reporter, :file_dependencies,
|
|
44
|
+
:analyzed_files, :unresolved_self_calls
|
|
37
45
|
|
|
38
46
|
# @param configuration [Rigor::Configuration]
|
|
39
47
|
# @param explain [Boolean] surface fail-soft fallback events
|
|
@@ -82,10 +90,11 @@ module Rigor
|
|
|
82
90
|
# (bundler / lockfile / collection discovery, RbsLoader
|
|
83
91
|
# construction). Pool mode ignores the override — each
|
|
84
92
|
# worker continues to build its own Environment.
|
|
85
|
-
def initialize(configuration:, explain: false, # rubocop:disable Metrics/ParameterLists
|
|
93
|
+
def initialize(configuration:, explain: false, # rubocop:disable Metrics/ParameterLists,Metrics/AbcSize,Metrics/MethodLength
|
|
86
94
|
cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT),
|
|
87
95
|
plugin_requirer: nil, workers: 0, collect_stats: true,
|
|
88
|
-
buffer: nil, prebuilt: nil, environment: nil
|
|
96
|
+
buffer: nil, prebuilt: nil, environment: nil,
|
|
97
|
+
record_dependencies: false, record_self_calls: false, analyze_only: nil)
|
|
89
98
|
@configuration = configuration
|
|
90
99
|
@explain = explain
|
|
91
100
|
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
@@ -95,6 +104,35 @@ module Rigor
|
|
|
95
104
|
@buffer = buffer
|
|
96
105
|
@prebuilt = prebuilt
|
|
97
106
|
@environment_override = environment
|
|
107
|
+
# ADR-46 slice 1 — opt-in cross-file dependency recording. Off by
|
|
108
|
+
# default; when true, `analyze_file` records each file's
|
|
109
|
+
# cross-file reads into `file_dependencies` (the incremental
|
|
110
|
+
# cache, a later slice, consumes them).
|
|
111
|
+
@record_dependencies = record_dependencies
|
|
112
|
+
# ADR-24 slice 4a — opt-in unresolved-implicit-self-call recording.
|
|
113
|
+
# Off by default; when true, `analyze_file` activates the engine
|
|
114
|
+
# choke-point recorder and collects each file's misses into
|
|
115
|
+
# `unresolved_self_calls` (a later closed-class-gated rule consumes
|
|
116
|
+
# them). Purely observational — diagnostics are byte-identical.
|
|
117
|
+
@record_self_calls = record_self_calls
|
|
118
|
+
@unresolved_self_calls = {}
|
|
119
|
+
# Memoised activation decision for the `call.self-undefined-method`
|
|
120
|
+
# rule (nil = not yet computed). See `self_undefined_rule_active?`.
|
|
121
|
+
@self_undefined_rule_active = nil
|
|
122
|
+
@analyzed_files = [].freeze
|
|
123
|
+
# In-memory source map for `#run_source` — `{ logical_path => source
|
|
124
|
+
# String }`. When set, `parse_source` reads bytes from here instead
|
|
125
|
+
# of disk and `expand_paths` accepts the (possibly non-existent)
|
|
126
|
+
# logical path. nil on a normal disk-backed run.
|
|
127
|
+
@in_memory_sources = nil
|
|
128
|
+
# ADR-46 slice 2 — the subset-analysis hook. When set (a collection
|
|
129
|
+
# of paths), the whole-project pre-pass still runs over every file
|
|
130
|
+
# (so the cross-file index is complete), but only files in this set
|
|
131
|
+
# are analyzed for diagnostics — the body tier re-analyses the
|
|
132
|
+
# affected closure and serves the rest from the per-file cache.
|
|
133
|
+
# `nil` (the default) analyzes everything.
|
|
134
|
+
@analyze_only = analyze_only && Set.new(analyze_only)
|
|
135
|
+
@file_dependencies = {}
|
|
98
136
|
@plugin_registry = Plugin::Registry::EMPTY
|
|
99
137
|
@dependency_source_index = DependencySourceInference::Index::EMPTY
|
|
100
138
|
@rbs_extended_reporter = RbsExtended::Reporter.new
|
|
@@ -103,16 +141,27 @@ module Rigor
|
|
|
103
141
|
# `#run` resets these for each invocation; pre-seed them to
|
|
104
142
|
# empty containers so `build_run_stats` / `pre_file_diagnostics`
|
|
105
143
|
# (private, called only from `#run`) can read them without
|
|
106
|
-
# nil-guards.
|
|
144
|
+
# nil-guards. Kept inline (not a helper) so the engine's own
|
|
145
|
+
# flow analysis sees the ivars established in the constructor.
|
|
107
146
|
@class_decl_paths_snapshot = {}.freeze
|
|
108
147
|
@signature_paths_snapshot = [].freeze
|
|
148
|
+
@synthesized_namespaces_snapshot = [].freeze
|
|
149
|
+
# `rigor:v1:conforms-to` results, snapshotted from the
|
|
150
|
+
# per-run RBS env in `analyze_files_sequentially` (gated on
|
|
151
|
+
# the project declaring `signature_paths:`) and drained by
|
|
152
|
+
# `conforms_to_diagnostics`. Inline default per the comment
|
|
153
|
+
# above so the engine's own flow analysis sees it seeded.
|
|
154
|
+
@conformance_results_snapshot = [].freeze
|
|
109
155
|
@cached_plugin_prepare_diagnostics = [].freeze
|
|
110
156
|
@project_discovered_classes = {}.freeze
|
|
111
157
|
@project_discovered_def_nodes = {}.freeze
|
|
112
158
|
@project_discovered_def_sources = {}.freeze
|
|
113
159
|
@project_discovered_superclasses = {}.freeze
|
|
114
160
|
@project_discovered_includes = {}.freeze
|
|
161
|
+
@project_discovered_class_sources = {}.freeze
|
|
115
162
|
@project_discovered_method_visibilities = {}.freeze
|
|
163
|
+
@project_discovered_methods = {}.freeze
|
|
164
|
+
@project_data_member_layouts = {}.freeze
|
|
116
165
|
end
|
|
117
166
|
|
|
118
167
|
# ADR-pending editor mode — present when the runner is wired
|
|
@@ -141,6 +190,8 @@ module Rigor
|
|
|
141
190
|
expansion = expand_paths(paths)
|
|
142
191
|
@class_decl_paths_snapshot = {}.freeze
|
|
143
192
|
@signature_paths_snapshot = []
|
|
193
|
+
@synthesized_namespaces_snapshot = []
|
|
194
|
+
@conformance_results_snapshot = []
|
|
144
195
|
|
|
145
196
|
if @prebuilt
|
|
146
197
|
adopt_prebuilt_project_scan(@prebuilt)
|
|
@@ -148,18 +199,212 @@ module Rigor
|
|
|
148
199
|
run_project_pre_passes(expansion: expansion)
|
|
149
200
|
end
|
|
150
201
|
|
|
151
|
-
diagnostics =
|
|
152
|
-
diagnostics += analyze_files(target_files(expansion))
|
|
153
|
-
diagnostics += rbs_extended_reporter_diagnostics
|
|
154
|
-
diagnostics += boundary_cross_diagnostics
|
|
155
|
-
diagnostics += source_rbs_synthesis_diagnostics
|
|
202
|
+
diagnostics = compute_run_diagnostics(expansion)
|
|
156
203
|
|
|
157
204
|
Result.new(
|
|
158
205
|
diagnostics: apply_severity_profile(diagnostics),
|
|
159
|
-
stats:
|
|
206
|
+
stats: stats_for_run(wall_started_at: wall_started_at, expansion: expansion)
|
|
160
207
|
)
|
|
161
208
|
end
|
|
162
209
|
|
|
210
|
+
# Analyze a single source String in memory, without writing it to
|
|
211
|
+
# disk — a clean entry point for embedders (LSP / editor mode) and a
|
|
212
|
+
# faster spec path than the per-call tmpdir + chdir. The source is
|
|
213
|
+
# bound to `path` (purely a logical identity carried in diagnostic
|
|
214
|
+
# locations; it need not exist on disk). The full run machinery still
|
|
215
|
+
# runs — environment build, plugin `prepare`, severity profile — so
|
|
216
|
+
# the result matches a one-file disk run; only the cross-file project
|
|
217
|
+
# pre-pass is empty (there is one file, and the per-file indexer
|
|
218
|
+
# self-discovers its own classes / defs).
|
|
219
|
+
#
|
|
220
|
+
# @param source [String] Ruby source to analyze.
|
|
221
|
+
# @param path [String] logical path for diagnostic locations.
|
|
222
|
+
# @return [Result]
|
|
223
|
+
def run_source(source:, path: "(source).rb")
|
|
224
|
+
@in_memory_sources = { path => source }
|
|
225
|
+
run([path])
|
|
226
|
+
ensure
|
|
227
|
+
@in_memory_sources = nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# ADR-46 — the project file set that a run over `paths` would
|
|
231
|
+
# analyze, computed by globbing only (no RBS environment build), so
|
|
232
|
+
# the incremental fingerprint can be derived cheaply on the warm path
|
|
233
|
+
# before deciding whether to build the env at all.
|
|
234
|
+
def analysis_file_set(paths = @configuration.paths)
|
|
235
|
+
expand_paths(paths).fetch(:files)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# ADR-46 §2 — inverts {#file_dependencies} into the reverse edge the
|
|
239
|
+
# incremental step walks: `dependents[X] = { A : A read a
|
|
240
|
+
# declaration / body from X }`. On an edit to X, the body tier
|
|
241
|
+
# (slice 2) re-analyses `{X} ∪ dependents[X]` and serves every other
|
|
242
|
+
# file from the per-file cache. Built on demand from the recorded
|
|
243
|
+
# `sources` sets (so it reflects whatever `analyze_file` captured —
|
|
244
|
+
# empty unless the runner was constructed with
|
|
245
|
+
# `record_dependencies: true`). The negative (`missing`) edges are
|
|
246
|
+
# NOT inverted here: they feed the structural tier (slice 3), which
|
|
247
|
+
# re-checks a consumer when a name it looked up and did not resolve
|
|
248
|
+
# later appears.
|
|
249
|
+
def file_dependents
|
|
250
|
+
Incremental.invert(@file_dependencies.transform_values(&:sources))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# ADR-46 slice 4 — per-symbol body fingerprints, computed from the
|
|
254
|
+
# project pre-pass def index. Returns a frozen hash of the form:
|
|
255
|
+
# { "path/to/file.rb" => { "ClassName#method" => sha256_hex, … }, … }
|
|
256
|
+
# Used by {Analysis::IncrementalSession} to detect which symbols in a
|
|
257
|
+
# changed file actually changed bodies, so only callers of those
|
|
258
|
+
# specific symbols are re-checked. Only meaningful after a run that
|
|
259
|
+
# populated `@project_discovered_def_nodes` (i.e. any full or subset
|
|
260
|
+
# analysis); returns an empty frozen hash before the first run.
|
|
261
|
+
def symbol_fingerprints
|
|
262
|
+
result = Hash.new { |h, k| h[k] = {} }
|
|
263
|
+
@project_discovered_def_sources.each do |class_name, methods|
|
|
264
|
+
methods.each do |method_sym, path_line|
|
|
265
|
+
path = path_line.split(":", 2).first
|
|
266
|
+
node = @project_discovered_def_nodes.dig(class_name, method_sym)
|
|
267
|
+
next unless node
|
|
268
|
+
|
|
269
|
+
result[path]["#{class_name}##{method_sym}"] =
|
|
270
|
+
Digest::SHA256.hexdigest(node.location.slice)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
result.transform_values(&:freeze).freeze
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# ADR-46 slice 3 — per-file set of the qualified class/module names
|
|
277
|
+
# declared in that file. Used to detect a class that *appeared* in an
|
|
278
|
+
# edit so a subclass whose ancestor was previously undefined (and so
|
|
279
|
+
# recorded a negative class edge) is re-checked. Inverts the project
|
|
280
|
+
# class-source attribution (class → declaring files).
|
|
281
|
+
def class_declarations
|
|
282
|
+
result = Hash.new { |hash, key| hash[key] = Set.new }
|
|
283
|
+
@project_discovered_class_sources.each do |class_name, files|
|
|
284
|
+
files.each { |file| result[file] << class_name }
|
|
285
|
+
end
|
|
286
|
+
result.transform_values(&:freeze).freeze
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# ADR-45 — unchanged-project fast path. Serves the whole run's
|
|
290
|
+
# (pre-severity-profile) diagnostics from one record-and-validate
|
|
291
|
+
# cache entry when every file the previous run read is unchanged,
|
|
292
|
+
# skipping the dominant per-file inference. The dependency set is
|
|
293
|
+
# collected AFTER the run (so it captures files the plugins read
|
|
294
|
+
# mid-analysis, e.g. a Pundit policy) and re-validated on the next
|
|
295
|
+
# run; the entry is keyed on the inputs known up front (config, gem
|
|
296
|
+
# / engine versions, analyzed-path set).
|
|
297
|
+
def compute_run_diagnostics(expansion)
|
|
298
|
+
@run_served_from_cache = false
|
|
299
|
+
return assemble_run_diagnostics(expansion) unless run_result_cacheable?
|
|
300
|
+
|
|
301
|
+
environment = resolve_sequential_environment(source_files: target_files(expansion))
|
|
302
|
+
rbs_descriptor = environment&.rbs_loader ? Cache::RbsDescriptor.build(environment.rbs_loader) : Cache::Descriptor.new
|
|
303
|
+
key_descriptor = run_key_descriptor(expansion, rbs_descriptor)
|
|
304
|
+
return assemble_run_diagnostics(expansion, environment: environment) if key_descriptor.nil?
|
|
305
|
+
|
|
306
|
+
computed = false
|
|
307
|
+
diagnostics = @cache_store.fetch_or_validate(
|
|
308
|
+
producer_id: "analysis.run-diagnostics", key_descriptor: key_descriptor
|
|
309
|
+
) do
|
|
310
|
+
computed = true
|
|
311
|
+
diags = assemble_run_diagnostics(expansion, environment: environment)
|
|
312
|
+
[diags, run_dependency_descriptor(expansion, rbs_descriptor)]
|
|
313
|
+
end
|
|
314
|
+
@run_served_from_cache = !computed
|
|
315
|
+
diagnostics
|
|
316
|
+
rescue StandardError
|
|
317
|
+
# The result cache must never break a run. If anything in the
|
|
318
|
+
# cache path fails, fall back to a direct, uncached analysis.
|
|
319
|
+
@run_served_from_cache = false
|
|
320
|
+
assemble_run_diagnostics(expansion)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def assemble_run_diagnostics(expansion, environment: nil)
|
|
324
|
+
diagnostics = pre_file_diagnostics(expansion)
|
|
325
|
+
# ADR-46 — record which project files this run actually analyzed
|
|
326
|
+
# (the `analyze_only` subset, or all of them). The incremental
|
|
327
|
+
# orchestrator serves every analyzed-but-not-affected file from the
|
|
328
|
+
# per-file cache, so it needs the full analyzed set to subtract the
|
|
329
|
+
# affected closure from.
|
|
330
|
+
targets = target_files(expansion)
|
|
331
|
+
@analyzed_files = targets
|
|
332
|
+
diagnostics += analyze_files(targets, environment: environment)
|
|
333
|
+
diagnostics += rbs_synthesized_namespace_diagnostics
|
|
334
|
+
diagnostics += conforms_to_diagnostics
|
|
335
|
+
diagnostics += rbs_extended_reporter_diagnostics
|
|
336
|
+
diagnostics += boundary_cross_diagnostics
|
|
337
|
+
diagnostics + source_rbs_synthesis_diagnostics
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# A cache hit skipped the analysis, so the per-run stats (wall
|
|
341
|
+
# split, RBS-class counts, …) were never gathered — report none
|
|
342
|
+
# rather than the stale snapshot defaults.
|
|
343
|
+
def stats_for_run(wall_started_at:, expansion:)
|
|
344
|
+
return nil unless @collect_stats
|
|
345
|
+
return nil if @run_served_from_cache
|
|
346
|
+
|
|
347
|
+
build_run_stats(wall_started_at: wall_started_at, expansion: expansion)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Cacheable only for a full sequential project run with a writable
|
|
351
|
+
# cache and no per-buffer / prebuilt override — every other mode has
|
|
352
|
+
# a different result identity (pool workers read in separate
|
|
353
|
+
# processes; editor mode is per-buffer; prebuilt is the LSP path).
|
|
354
|
+
def run_result_cacheable?
|
|
355
|
+
!@cache_store.nil? && !@cache_store.read_only? &&
|
|
356
|
+
@buffer.nil? && @prebuilt.nil? && !pool_mode?
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Stable cache key inputs — known before the run: a digest of the
|
|
360
|
+
# resolved configuration, the engine + rbs versions + `--explain`,
|
|
361
|
+
# and the analyzed-path SET (adding/removing a file changes the
|
|
362
|
+
# key; editing one is caught by dependency validation). nil disables
|
|
363
|
+
# the cache for this run rather than risking a malformed key.
|
|
364
|
+
def run_key_descriptor(expansion, rbs_descriptor)
|
|
365
|
+
Cache::Descriptor.new(
|
|
366
|
+
gems: rbs_descriptor.gems,
|
|
367
|
+
configs: rbs_descriptor.configs + [
|
|
368
|
+
config_hash_entry("configuration", Marshal.dump(@configuration.to_h)),
|
|
369
|
+
config_hash_entry("engine", "#{Rigor::VERSION}:#{Cache::Descriptor::SCHEMA_VERSION}:#{@explain}"),
|
|
370
|
+
config_hash_entry("paths", expansion.fetch(:files).sort.join("\n"))
|
|
371
|
+
]
|
|
372
|
+
)
|
|
373
|
+
rescue StandardError
|
|
374
|
+
nil
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Files the run actually depended on, collected AFTER it ran:
|
|
378
|
+
# every analyzed file, every RBS `sig` file (`rbs_descriptor.files`),
|
|
379
|
+
# and every file each plugin read (complete post-run, so reads made
|
|
380
|
+
# mid-analysis are included). Re-digested on the next run by
|
|
381
|
+
# {Descriptor#fresh?}.
|
|
382
|
+
def run_dependency_descriptor(expansion, rbs_descriptor)
|
|
383
|
+
entries = analyzed_file_entries(expansion) + rbs_descriptor.files
|
|
384
|
+
@plugin_registry.plugins.each do |plugin|
|
|
385
|
+
# Read the boundary WITHOUT triggering its lazy `@io_boundary ||=`
|
|
386
|
+
# initializer: plugin instances are frozen after the run, and a
|
|
387
|
+
# plugin that never built a boundary read no files through it, so
|
|
388
|
+
# it contributes no dependencies.
|
|
389
|
+
boundary = plugin.instance_variable_get(:@io_boundary)
|
|
390
|
+
entries.concat(boundary.cache_descriptor.files) if boundary
|
|
391
|
+
end
|
|
392
|
+
Cache::Descriptor.new(files: entries)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def analyzed_file_entries(expansion)
|
|
396
|
+
expansion.fetch(:files).map do |path|
|
|
397
|
+
physical = @buffer ? @buffer.resolve(path) : path
|
|
398
|
+
Cache::Descriptor::FileEntry.new(
|
|
399
|
+
path: physical, comparator: :digest, value: Digest::SHA256.file(physical).hexdigest
|
|
400
|
+
)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def config_hash_entry(key, payload)
|
|
405
|
+
Cache::Descriptor::ConfigEntry.new(key: key, value_hash: Digest::SHA256.hexdigest(payload))
|
|
406
|
+
end
|
|
407
|
+
|
|
163
408
|
# Runs every project-wide pre-pass (`load_plugins` +
|
|
164
409
|
# `plugin#prepare` + dependency-source builder +
|
|
165
410
|
# synthetic-method scanner + project-patched scanner)
|
|
@@ -200,7 +445,7 @@ module Rigor
|
|
|
200
445
|
# downstream `#run` body expects. Extracted so
|
|
201
446
|
# `#prepare_project_scan` and the prebuilt-less `#run` path
|
|
202
447
|
# share one implementation.
|
|
203
|
-
def run_project_pre_passes(expansion:)
|
|
448
|
+
def run_project_pre_passes(expansion:) # rubocop:disable Metrics/AbcSize
|
|
204
449
|
@plugin_registry = load_plugins
|
|
205
450
|
@dependency_source_index = DependencySourceInference::Builder.build(@configuration.dependencies)
|
|
206
451
|
# ADR-18 slice 3 — plugin prepare MUST run before the
|
|
@@ -262,7 +507,10 @@ module Rigor
|
|
|
262
507
|
@project_discovered_def_sources = def_index.fetch(:def_sources)
|
|
263
508
|
@project_discovered_superclasses = def_index.fetch(:superclasses)
|
|
264
509
|
@project_discovered_includes = def_index.fetch(:includes)
|
|
510
|
+
@project_discovered_class_sources = def_index.fetch(:class_sources)
|
|
265
511
|
@project_discovered_method_visibilities = def_index.fetch(:method_visibilities)
|
|
512
|
+
@project_discovered_methods = def_index.fetch(:methods)
|
|
513
|
+
@project_data_member_layouts = def_index.fetch(:data_member_layouts)
|
|
266
514
|
end
|
|
267
515
|
|
|
268
516
|
# Internal: adopts a frozen {ProjectScan} snapshot supplied
|
|
@@ -296,21 +544,36 @@ module Rigor
|
|
|
296
544
|
# method returns — holding it as long-lived state added
|
|
297
545
|
# memory pressure that surfaced as a Bus Error during the
|
|
298
546
|
# spec suite under Ruby 4.0 + rbs 4.0.2.
|
|
299
|
-
def analyze_files(files)
|
|
547
|
+
def analyze_files(files, environment: nil)
|
|
300
548
|
return [] if files.empty?
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
549
|
+
return dispatch_pool(files) if pool_mode?
|
|
550
|
+
|
|
551
|
+
analyze_files_sequentially(files, environment || resolve_sequential_environment(source_files: files))
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def analyze_files_sequentially(files, environment)
|
|
555
|
+
# Snapshot the small synthesized-namespace name list (NOT the
|
|
556
|
+
# env — see the method comment) so #run can surface the
|
|
557
|
+
# malformed-RBS `:info` diagnostic without rebuilding the env.
|
|
558
|
+
# Gated on the project actually declaring `signature_paths:`:
|
|
559
|
+
# synthesis only matters for the project's own RBS, and
|
|
560
|
+
# `#synthesized_namespaces` forces the (otherwise-lazy) RBS env
|
|
561
|
+
# to build — doing so when there is no project sig set would
|
|
562
|
+
# warm `.rigor/cache` on a bare `--no-stats` run.
|
|
563
|
+
@synthesized_namespaces_snapshot =
|
|
564
|
+
project_signature_paths? ? (environment.rbs_loader&.synthesized_namespaces || []) : []
|
|
565
|
+
# `rigor:v1:conforms-to` lives only in the project's own
|
|
566
|
+
# `signature_paths:` RBS, so gate the scan the same way and
|
|
567
|
+
# reuse the already-built env (no extra RBS load).
|
|
568
|
+
@conformance_results_snapshot =
|
|
569
|
+
project_signature_paths? ? RbsExtended::ConformanceChecker.scan(environment.rbs_loader) : []
|
|
570
|
+
result = files.flat_map { |path| analyze_file(path, environment) }
|
|
571
|
+
if @collect_stats
|
|
572
|
+
loader = environment.rbs_loader
|
|
573
|
+
@class_decl_paths_snapshot = loader&.class_decl_paths || {}.freeze
|
|
574
|
+
@signature_paths_snapshot = loader&.signature_paths || [].freeze
|
|
313
575
|
end
|
|
576
|
+
result
|
|
314
577
|
end
|
|
315
578
|
|
|
316
579
|
# Sequential-mode environment resolver. Returns the supplied
|
|
@@ -448,6 +711,11 @@ module Rigor
|
|
|
448
711
|
# buffer".
|
|
449
712
|
def target_files(expansion)
|
|
450
713
|
files = expansion.fetch(:files)
|
|
714
|
+
# ADR-46 slice 2 — restrict the analyzed set to the affected
|
|
715
|
+
# closure while the pre-pass (run separately over `expansion`'s
|
|
716
|
+
# full file list) keeps the cross-file index complete. Buffer mode
|
|
717
|
+
# takes precedence — its single logical path is the analyzed set.
|
|
718
|
+
files = files.select { |path| @analyze_only.include?(path) } if @analyze_only
|
|
451
719
|
return files if @buffer.nil?
|
|
452
720
|
|
|
453
721
|
[@buffer.logical_path]
|
|
@@ -1116,6 +1384,114 @@ module Rigor
|
|
|
1116
1384
|
[build_rbs_coverage_missing_diagnostic(missing)]
|
|
1117
1385
|
end
|
|
1118
1386
|
|
|
1387
|
+
# Robustness uplift companion (ADR-5) — when the project's
|
|
1388
|
+
# `signature_paths:` RBS declared qualified names without their
|
|
1389
|
+
# enclosing namespace, `RbsLoader` synthesizes the missing
|
|
1390
|
+
# `module`s so the otherwise-inert signatures resolve. Surface a
|
|
1391
|
+
# single `:info` diagnostic naming them so the user knows their
|
|
1392
|
+
# sig set is malformed (`rbs validate` rejects it) and can fix it
|
|
1393
|
+
# at the source. Authored `:info`: the analysis already succeeded;
|
|
1394
|
+
# this is advisory, never a gate. Empty for a well-formed sig set.
|
|
1395
|
+
def rbs_synthesized_namespace_diagnostics
|
|
1396
|
+
synthesized = @synthesized_namespaces_snapshot
|
|
1397
|
+
return [] if synthesized.nil? || synthesized.empty?
|
|
1398
|
+
|
|
1399
|
+
[build_rbs_synthesized_namespace_diagnostic(synthesized)]
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
# Maps the per-run `rigor:v1:conforms-to` scan results into
|
|
1403
|
+
# diagnostics (spec: `rbs-extended.md` § "Explicit conformance
|
|
1404
|
+
# directive"). A class that declares `conforms-to _Interface`
|
|
1405
|
+
# but is missing a required interface method surfaces as
|
|
1406
|
+
# `rbs_extended.unsatisfied-conformance`; an unresolvable
|
|
1407
|
+
# interface name surfaces as `dynamic.rbs-extended.unresolved`
|
|
1408
|
+
# `:info` (the same fail-soft channel the other directive
|
|
1409
|
+
# parsers use). Empty for a project with no directive, a
|
|
1410
|
+
# well-formed conformance, or a non-sequential pool run (the
|
|
1411
|
+
# snapshot mirrors `synthesized_namespaces`).
|
|
1412
|
+
def conforms_to_diagnostics
|
|
1413
|
+
results = @conformance_results_snapshot
|
|
1414
|
+
return [] if results.nil? || results.empty?
|
|
1415
|
+
|
|
1416
|
+
results.map { |record| build_conformance_diagnostic(record) }
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1419
|
+
def build_conformance_diagnostic(record)
|
|
1420
|
+
case record
|
|
1421
|
+
when RbsExtended::ConformanceChecker::Unsatisfied
|
|
1422
|
+
build_unsatisfied_conformance_diagnostic(record)
|
|
1423
|
+
when RbsExtended::ConformanceChecker::IncompatibleSignature
|
|
1424
|
+
build_incompatible_signature_diagnostic(record)
|
|
1425
|
+
else # UnresolvedInterface
|
|
1426
|
+
build_reporter_diagnostic(
|
|
1427
|
+
record.location,
|
|
1428
|
+
rule: "dynamic.rbs-extended.unresolved",
|
|
1429
|
+
message: "`#{record.class_name}` declares `conforms-to #{record.interface_name}` but " \
|
|
1430
|
+
"interface `#{record.interface_name}` is not loaded. Check for a typo or add " \
|
|
1431
|
+
"the `sig`/library that declares it to the RBS load path."
|
|
1432
|
+
)
|
|
1433
|
+
end
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
def build_unsatisfied_conformance_diagnostic(record)
|
|
1437
|
+
path, line, column = location_fields(record.location)
|
|
1438
|
+
Diagnostic.new(
|
|
1439
|
+
path: path, line: line, column: column,
|
|
1440
|
+
message: "`#{record.class_name}` declares `conforms-to #{record.interface_name}` " \
|
|
1441
|
+
"but does not provide #{pluralize_methods(record.missing_methods)}: " \
|
|
1442
|
+
"#{record.missing_methods.map { |m| "`##{m}`" }.join(', ')}. Implement the " \
|
|
1443
|
+
"missing method(s) or remove the directive.",
|
|
1444
|
+
severity: :warning,
|
|
1445
|
+
rule: "rbs_extended.unsatisfied-conformance",
|
|
1446
|
+
source_family: :builtin
|
|
1447
|
+
)
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
def build_incompatible_signature_diagnostic(record)
|
|
1451
|
+
path, line, column = location_fields(record.location)
|
|
1452
|
+
Diagnostic.new(
|
|
1453
|
+
path: path, line: line, column: column,
|
|
1454
|
+
message: "`#{record.class_name}##{record.method_name}` does not satisfy " \
|
|
1455
|
+
"`conforms-to #{record.interface_name}`: #{record.detail}. Adjust the " \
|
|
1456
|
+
"signature to a subtype of the interface contract.",
|
|
1457
|
+
severity: :warning,
|
|
1458
|
+
rule: "rbs_extended.unsatisfied-conformance",
|
|
1459
|
+
source_family: :builtin,
|
|
1460
|
+
method_name: record.method_name
|
|
1461
|
+
)
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
def pluralize_methods(methods)
|
|
1465
|
+
methods.size == 1 ? "required method" : "#{methods.size} required methods"
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
# True when the project declares its own `signature_paths:` (the
|
|
1469
|
+
# only place the qualified-name-without-namespace mistake lives).
|
|
1470
|
+
def project_signature_paths?
|
|
1471
|
+
paths = @configuration.signature_paths
|
|
1472
|
+
!(paths.nil? || paths.empty?)
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
def build_rbs_synthesized_namespace_diagnostic(synthesized)
|
|
1476
|
+
sample_size = 5
|
|
1477
|
+
sample = synthesized.first(sample_size)
|
|
1478
|
+
suffix = synthesized.size > sample_size ? ", and #{synthesized.size - sample_size} more" : ""
|
|
1479
|
+
Diagnostic.new(
|
|
1480
|
+
path: ".rigor.yml",
|
|
1481
|
+
line: 1,
|
|
1482
|
+
column: 1,
|
|
1483
|
+
message: "#{synthesized.size} RBS namespace(s) under `signature_paths:` are " \
|
|
1484
|
+
"referenced by qualified declarations (e.g. `class Foo::Bar`) but never " \
|
|
1485
|
+
"declared: #{sample.join(', ')}#{suffix}. `rbs validate` rejects this; " \
|
|
1486
|
+
"Rigor synthesized the missing `module`(s) so the signatures still " \
|
|
1487
|
+
"resolve. Declare each (`module <name>` / `class <name>`) in your RBS to " \
|
|
1488
|
+
"make the sig set valid upstream.",
|
|
1489
|
+
severity: :info,
|
|
1490
|
+
rule: "rbs.coverage.synthesized-namespace",
|
|
1491
|
+
source_family: :builtin
|
|
1492
|
+
)
|
|
1493
|
+
end
|
|
1494
|
+
|
|
1119
1495
|
def build_rbs_coverage_missing_diagnostic(missing)
|
|
1120
1496
|
sample_size = 5
|
|
1121
1497
|
sample = missing.first(sample_size).map(&:gem_name)
|
|
@@ -1328,8 +1704,9 @@ module Rigor
|
|
|
1328
1704
|
end
|
|
1329
1705
|
|
|
1330
1706
|
def collect_plugin_diagnostics(plugin, path, root, scope)
|
|
1331
|
-
raw = plugin.diagnostics_for_file(path: path, scope: scope, root: root)
|
|
1332
|
-
|
|
1707
|
+
raw = Array(plugin.diagnostics_for_file(path: path, scope: scope, root: root))
|
|
1708
|
+
raw += plugin.node_rule_diagnostics(path: path, scope: scope, root: root)
|
|
1709
|
+
raw.map { |diagnostic| stamp_plugin_diagnostic(diagnostic, plugin.manifest.id) }
|
|
1333
1710
|
rescue StandardError => e
|
|
1334
1711
|
[plugin_runtime_error_diagnostic(path, plugin, e)]
|
|
1335
1712
|
end
|
|
@@ -1397,7 +1774,8 @@ module Rigor
|
|
|
1397
1774
|
|
|
1398
1775
|
def accept_as_ruby_file?(path)
|
|
1399
1776
|
(File.file?(path) && path.end_with?(".rb")) ||
|
|
1400
|
-
(@buffer && path == @buffer.logical_path)
|
|
1777
|
+
(@buffer && path == @buffer.logical_path) ||
|
|
1778
|
+
@in_memory_sources&.key?(path)
|
|
1401
1779
|
end
|
|
1402
1780
|
|
|
1403
1781
|
# `Configuration#exclude_patterns` is a list of glob patterns
|
|
@@ -1436,6 +1814,10 @@ module Rigor
|
|
|
1436
1814
|
# LOGICAL path. Non-binding paths go through the cheaper
|
|
1437
1815
|
# `Prism.parse_file` codepath unchanged.
|
|
1438
1816
|
def parse_source(path)
|
|
1817
|
+
if @in_memory_sources&.key?(path)
|
|
1818
|
+
return Prism.parse(@in_memory_sources[path], filepath: path, version: @configuration.target_ruby)
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1439
1821
|
physical = @buffer ? @buffer.resolve(path) : path
|
|
1440
1822
|
return Prism.parse_file(physical, version: @configuration.target_ruby) if physical == path
|
|
1441
1823
|
|
|
@@ -1463,10 +1845,35 @@ module Rigor
|
|
|
1463
1845
|
unless @project_discovered_method_visibilities.empty?
|
|
1464
1846
|
scope = scope.with_discovered_method_visibilities(@project_discovered_method_visibilities)
|
|
1465
1847
|
end
|
|
1848
|
+
scope = scope.with_discovered_methods(@project_discovered_methods) unless @project_discovered_methods.empty?
|
|
1849
|
+
scope = scope.with_data_member_layouts(@project_data_member_layouts) unless @project_data_member_layouts.empty?
|
|
1850
|
+
# ADR-46 slice 1 — the class-declaration source map is read only by
|
|
1851
|
+
# the ancestry accessors during dependency recording, so seed it
|
|
1852
|
+
# only when recording is on; a normal run never carries it.
|
|
1853
|
+
if @record_dependencies && !@project_discovered_class_sources.empty?
|
|
1854
|
+
scope = scope.with_discovered_class_sources(@project_discovered_class_sources)
|
|
1855
|
+
end
|
|
1466
1856
|
scope
|
|
1467
1857
|
end
|
|
1468
1858
|
|
|
1469
|
-
|
|
1859
|
+
# ADR-46 slice 1 — when dependency recording is enabled, wrap the
|
|
1860
|
+
# per-file analysis so the cross-file reads its inference makes are
|
|
1861
|
+
# captured into `file_dependencies[path]`. Off by default: a normal
|
|
1862
|
+
# run calls the body directly and the instrumented `Scope` accessors
|
|
1863
|
+
# short-circuit on `DependencyRecorder.active? == false`. Recording
|
|
1864
|
+
# is observational, so diagnostics are byte-identical either way.
|
|
1865
|
+
def analyze_file(path, environment)
|
|
1866
|
+
return analyze_file_body(path, environment) unless @record_dependencies
|
|
1867
|
+
|
|
1868
|
+
diagnostics = nil
|
|
1869
|
+
record = DependencyRecorder.record_for(path) do
|
|
1870
|
+
diagnostics = analyze_file_body(path, environment)
|
|
1871
|
+
end
|
|
1872
|
+
@file_dependencies[path] = record
|
|
1873
|
+
diagnostics
|
|
1874
|
+
end
|
|
1875
|
+
|
|
1876
|
+
def analyze_file_body(path, environment) # rubocop:disable Metrics/MethodLength
|
|
1470
1877
|
parse_result = parse_source(path)
|
|
1471
1878
|
unless parse_result.errors.empty?
|
|
1472
1879
|
return [] if ErbTemplateDetector.template?(parse_result)
|
|
@@ -1475,11 +1882,20 @@ module Rigor
|
|
|
1475
1882
|
end
|
|
1476
1883
|
|
|
1477
1884
|
scope = seed_project_scope(Scope.empty(environment: environment, source_path: path))
|
|
1478
|
-
|
|
1885
|
+
# ADR-24 slice 4a/4 — record unresolved implicit-self calls during the
|
|
1886
|
+
# typing pass ONLY (not CheckRules, whose own `type_of` queries would
|
|
1887
|
+
# otherwise re-trigger the choke-point). `self_call_misses` feeds the
|
|
1888
|
+
# `call.self-undefined-method` collector; the recorder is inert unless
|
|
1889
|
+
# the rule is active or `record_self_calls:` opted in.
|
|
1890
|
+
index = nil
|
|
1891
|
+
self_call_record = with_self_call_recording(path) do
|
|
1892
|
+
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
1893
|
+
end
|
|
1479
1894
|
diagnostics = CheckRules.diagnose(
|
|
1480
1895
|
path: path,
|
|
1481
1896
|
root: parse_result.value,
|
|
1482
1897
|
scope_index: index,
|
|
1898
|
+
self_call_misses: self_call_record ? self_call_record.calls : [],
|
|
1483
1899
|
comments: parse_result.comments,
|
|
1484
1900
|
disabled_rules: @configuration.disabled_rules
|
|
1485
1901
|
)
|
|
@@ -1507,6 +1923,46 @@ module Rigor
|
|
|
1507
1923
|
]
|
|
1508
1924
|
end
|
|
1509
1925
|
|
|
1926
|
+
# ADR-24 slice 4a — runs `block` (the typing pass) with the self-call
|
|
1927
|
+
# recorder active when either the test-only `record_self_calls:` flag is
|
|
1928
|
+
# set or the `call.self-undefined-method` rule resolves to a firing
|
|
1929
|
+
# severity. Returns the frozen {SelfCallResolutionRecorder::Record}, or
|
|
1930
|
+
# nil when recording is inactive (the common path — one integer read).
|
|
1931
|
+
def with_self_call_recording(path, &)
|
|
1932
|
+
unless self_call_recording_active?
|
|
1933
|
+
yield
|
|
1934
|
+
return nil
|
|
1935
|
+
end
|
|
1936
|
+
|
|
1937
|
+
record = SelfCallResolutionRecorder.record_for(path, &)
|
|
1938
|
+
@unresolved_self_calls[path] = record
|
|
1939
|
+
record
|
|
1940
|
+
end
|
|
1941
|
+
|
|
1942
|
+
def self_call_recording_active?
|
|
1943
|
+
@record_self_calls || self_undefined_rule_active?
|
|
1944
|
+
end
|
|
1945
|
+
|
|
1946
|
+
# Memoised: the rule fires only when its resolved severity is not `:off`
|
|
1947
|
+
# and it is not in `disable:`. Default profiles map it to `:off`, so a
|
|
1948
|
+
# normal run never activates the recorder (pending the external WD4
|
|
1949
|
+
# corpus FP gate — see ADR-24 § "Slice 4"); a project opts in via
|
|
1950
|
+
# `severity_overrides:`.
|
|
1951
|
+
def self_undefined_rule_active?
|
|
1952
|
+
return @self_undefined_rule_active unless @self_undefined_rule_active.nil?
|
|
1953
|
+
|
|
1954
|
+
rule = CheckRules::RULE_SELF_UNDEFINED_METHOD
|
|
1955
|
+
@self_undefined_rule_active =
|
|
1956
|
+
if @configuration.disabled_rules.include?(rule) || @configuration.disabled_rules.include?("call")
|
|
1957
|
+
false
|
|
1958
|
+
else
|
|
1959
|
+
Configuration::SeverityProfile.resolve(
|
|
1960
|
+
rule: rule, authored_severity: :warning,
|
|
1961
|
+
profile: @configuration.severity_profile, overrides: @configuration.severity_overrides
|
|
1962
|
+
) != :off
|
|
1963
|
+
end
|
|
1964
|
+
end
|
|
1965
|
+
|
|
1510
1966
|
# v0.0.2 #10 — fail-soft fallback explanation. When
|
|
1511
1967
|
# `--explain` is set the runner additionally walks the
|
|
1512
1968
|
# file with `Rigor::Inference::CoverageScanner` and emits
|