rigortype 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +159 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +187 -55
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +192 -0
- data/lib/rigor/plugin/registry.rb +264 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -72,7 +72,13 @@ module Rigor
|
|
|
72
72
|
}
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
# `watch:` covers every `.yml` / `.yaml` file under the locale
|
|
76
|
+
# search paths so the cache invalidates when locale files are
|
|
77
|
+
# added, removed, or edited (ADR-60 WD3). `@load_errors` is a
|
|
78
|
+
# producer-side capture: it is populated only when the block
|
|
79
|
+
# runs (a cache miss / a watched file changed), which is exactly
|
|
80
|
+
# when a malformed YAML must re-surface.
|
|
81
|
+
producer :locale_index, watch: -> { [[@locale_search_paths, "**/*.yml", "**/*.yaml"]] } do |_params|
|
|
76
82
|
loader = LocaleLoader.new(
|
|
77
83
|
io_boundary: io_boundary,
|
|
78
84
|
search_paths: @locale_search_paths
|
|
@@ -85,21 +91,19 @@ module Rigor
|
|
|
85
91
|
def init(_services)
|
|
86
92
|
@locale_search_paths = Array(config.fetch("locale_search_paths")).map(&:to_s)
|
|
87
93
|
@configured_locales = Array(config.fetch("configured_locales")).map(&:to_s)
|
|
88
|
-
@locale_index = nil
|
|
89
94
|
@load_errors = []
|
|
90
95
|
@load_errors_emitted = false
|
|
91
|
-
@runtime_error = nil
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
# File-level only: the once-per-run YAML load errors + the
|
|
95
99
|
# runtime (cache-load) error. Per-call `t('key')` validation runs
|
|
96
100
|
# over the engine-owned walk via the node_rule below (ADR-37). The
|
|
97
|
-
# locale index is lazily loaded + memoised by
|
|
101
|
+
# locale index is lazily loaded + memoised by `producer_value`.
|
|
98
102
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
99
|
-
index =
|
|
103
|
+
index = producer_value(:locale_index)
|
|
100
104
|
diagnostics = []
|
|
101
105
|
diagnostics.concat(consume_load_error_diagnostics(path)) unless @load_errors.empty?
|
|
102
|
-
diagnostics << runtime_error_diagnostic(path) if index.nil? &&
|
|
106
|
+
diagnostics << runtime_error_diagnostic(path) if index.nil? && producer_error(:locale_index)
|
|
103
107
|
diagnostics
|
|
104
108
|
end
|
|
105
109
|
|
|
@@ -107,39 +111,21 @@ module Rigor
|
|
|
107
111
|
# (the controller action), supplied by the node-rule NodeContext;
|
|
108
112
|
# the controller scope comes from the file path.
|
|
109
113
|
node_rule Prism::CallNode do |node, _scope, path, _fc, context|
|
|
110
|
-
index =
|
|
114
|
+
index = producer_value(:locale_index)
|
|
111
115
|
next [] if index.nil? || index.empty?
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
diagnostics_for(
|
|
118
|
+
Analyzer.violations_for(
|
|
119
|
+
call_node: node, locale_index: index, configured_locales: @configured_locales,
|
|
120
|
+
controller_scope: Analyzer.controller_scope_from_path(path),
|
|
121
|
+
action: context.enclosing_def&.name
|
|
122
|
+
),
|
|
123
|
+
path: path, node: node
|
|
124
|
+
)
|
|
120
125
|
end
|
|
121
126
|
|
|
122
127
|
private
|
|
123
128
|
|
|
124
|
-
def locale_index_or_nil
|
|
125
|
-
return @locale_index if @locale_index
|
|
126
|
-
|
|
127
|
-
# Pass an explicit descriptor covering every `.yml` / `.yaml`
|
|
128
|
-
# file under the configured locale search paths so the cache
|
|
129
|
-
# invalidates when locale files are added, removed, or edited.
|
|
130
|
-
# Without it the auto-built descriptor depends on the
|
|
131
|
-
# `IoBoundary`'s in-process read history — empty on the
|
|
132
|
-
# first call of a fresh process — so warm cache hits would
|
|
133
|
-
# serve stale `LocaleIndex` data and hide per-call load
|
|
134
|
-
# errors (a malformed YAML in one run would not surface
|
|
135
|
-
# when a healthy cache entry from an earlier run exists).
|
|
136
|
-
descriptor = glob_descriptor(@locale_search_paths, "**/*.yml", "**/*.yaml")
|
|
137
|
-
@locale_index = cache_for(:locale_index, params: {}, descriptor: descriptor).call
|
|
138
|
-
rescue StandardError => e
|
|
139
|
-
@runtime_error = "rigor-rails-i18n: failed to load locales: #{e.class}: #{e.message}"
|
|
140
|
-
nil
|
|
141
|
-
end
|
|
142
|
-
|
|
143
129
|
# The runner only invokes `diagnostics_for_file` for
|
|
144
130
|
# Ruby files (`paths:` is filtered to `.rb`). YAML
|
|
145
131
|
# parse errors therefore can't be anchored on the
|
|
@@ -161,9 +147,10 @@ module Rigor
|
|
|
161
147
|
end
|
|
162
148
|
|
|
163
149
|
def runtime_error_diagnostic(path)
|
|
150
|
+
error = producer_error(:locale_index)
|
|
164
151
|
Rigor::Analysis::Diagnostic.new(
|
|
165
152
|
path: path, line: 1, column: 1,
|
|
166
|
-
message:
|
|
153
|
+
message: "rigor-rails-i18n: failed to load locales: #{error.class}: #{error.message}",
|
|
167
154
|
severity: :warning,
|
|
168
155
|
rule: "load-error"
|
|
169
156
|
)
|
|
@@ -94,7 +94,12 @@ module Rigor
|
|
|
94
94
|
#
|
|
95
95
|
# Passes a `file_reader` lambda so the parser can follow
|
|
96
96
|
# `draw(:admin)` → `config/routes/admin.rb` partials.
|
|
97
|
-
|
|
97
|
+
# `watch:` (ADR-60 WD3) covers every `.rb` under `helper_paths:`
|
|
98
|
+
# so ADDING a helper file invalidates the table — the producer's
|
|
99
|
+
# own in-block reads (the routes file + the partials it follows
|
|
100
|
+
# via `draw`) are captured post-compute, but a brand-new helper
|
|
101
|
+
# file the prior run never read would otherwise read as fresh.
|
|
102
|
+
producer :helper_table, watch: -> { @helper_paths.map { |dir| [dir, "**/*.rb"] } } do |_params|
|
|
98
103
|
routes_dir = "#{File.dirname(@routes_file)}/routes"
|
|
99
104
|
file_reader = lambda do |name|
|
|
100
105
|
io_boundary.read_file("#{routes_dir}/#{name}")
|
|
@@ -132,14 +137,6 @@ module Rigor
|
|
|
132
137
|
HelperDiscoverer.discover(contents_per_path)
|
|
133
138
|
end
|
|
134
139
|
|
|
135
|
-
def pre_read_helper_files
|
|
136
|
-
each_helper_file do |path|
|
|
137
|
-
io_boundary.read_file(path)
|
|
138
|
-
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
139
|
-
next
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
140
|
def each_helper_file(&)
|
|
144
141
|
@helper_paths.each do |dir|
|
|
145
142
|
absolute = File.expand_path(dir)
|
|
@@ -214,15 +211,11 @@ module Rigor
|
|
|
214
211
|
return @helper_table if @helper_table_built
|
|
215
212
|
|
|
216
213
|
@helper_table_built = true
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
# `app/helpers/` MUST invalidate the helper_table cache
|
|
223
|
-
# so the new custom-helper set is picked up.
|
|
224
|
-
io_boundary.read_file(@routes_file)
|
|
225
|
-
pre_read_helper_files
|
|
214
|
+
# ADR-60 WD3 record-and-validate: the producer's in-block reads
|
|
215
|
+
# (the routes file + the partials it follows) are captured into
|
|
216
|
+
# the dependency descriptor AFTER the block runs, and the
|
|
217
|
+
# producer's `watch:` covers helper-file additions — so no
|
|
218
|
+
# priming read is needed here.
|
|
226
219
|
@helper_table = cache_for(:helper_table, params: {}).call
|
|
227
220
|
rescue Plugin::AccessDeniedError => e
|
|
228
221
|
@load_error = "rigor-rails-routes: #{e.message}"
|
|
@@ -147,7 +147,6 @@ module Rigor
|
|
|
147
147
|
block_as_methods: base.block_as_methods,
|
|
148
148
|
heredoc_templates: base.heredoc_templates,
|
|
149
149
|
trait_registries: base.trait_registries,
|
|
150
|
-
external_files: base.external_files,
|
|
151
150
|
hkt_registrations: base.hkt_registrations,
|
|
152
151
|
hkt_definitions: base.hkt_definitions,
|
|
153
152
|
signature_paths: base.signature_paths,
|
|
@@ -21,8 +21,8 @@ module Rigor
|
|
|
21
21
|
# { ... }` and `subject(:name) { ... }` declarations.
|
|
22
22
|
# `:subject` is the key for the implicit `subject { ... }`.
|
|
23
23
|
#
|
|
24
|
-
# Pillar 2 Slice 2 — used by the plugin's
|
|
25
|
-
# `
|
|
24
|
+
# Pillar 2 Slice 2 — used by the plugin's let-binding
|
|
25
|
+
# `dynamic_return` rule to bind `let`-named
|
|
26
26
|
# method-shape calls inside `it` bodies to the let
|
|
27
27
|
# block's inferred type.
|
|
28
28
|
class LetScopeIndex
|
|
@@ -48,6 +48,16 @@ module Rigor
|
|
|
48
48
|
@records.select { |rec| rec.contains?(line) }
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# ADR-52 slice 5a — every `let` / `subject` name declared
|
|
52
|
+
# anywhere in the file, across all describe scopes. Feeds the
|
|
53
|
+
# plugin's `dynamic_return file_methods:` gate: the engine only
|
|
54
|
+
# consults the rule for a call whose name appears here; the
|
|
55
|
+
# precise line-scoped resolution stays in `let_block_at`.
|
|
56
|
+
# @return [Array<Symbol>]
|
|
57
|
+
def let_names
|
|
58
|
+
@records.flat_map { |rec| rec.lets.keys }.uniq
|
|
59
|
+
end
|
|
60
|
+
|
|
51
61
|
# Resolves a `let` name at the given line by walking
|
|
52
62
|
# records innermost to outermost.
|
|
53
63
|
# @return [Prism::BlockNode, nil]
|
|
@@ -9,7 +9,7 @@ module Rigor
|
|
|
9
9
|
module Plugin
|
|
10
10
|
class Rspec < Rigor::Plugin::Base
|
|
11
11
|
# Pillar 2 Slice 1 — recognises `expect(x).to MATCHER`
|
|
12
|
-
# patterns at
|
|
12
|
+
# patterns at per-call recognition time and emits
|
|
13
13
|
# `post_return_facts` that narrow the named local on the
|
|
14
14
|
# post-call edge.
|
|
15
15
|
#
|
|
@@ -72,23 +72,31 @@ module Rigor
|
|
|
72
72
|
"`subject { described_class.new(...) }`.",
|
|
73
73
|
consumes: [
|
|
74
74
|
{ plugin_id: "factorybot", name: :factory_index, optional: true }
|
|
75
|
+
],
|
|
76
|
+
additional_initializers: [
|
|
77
|
+
# ADR-38 block-form: `before { @ivar = … }`, `let(:x) { @ivar = … }`,
|
|
78
|
+
# and `subject { @ivar = … }` establish ivar state before `it` bodies
|
|
79
|
+
# run. Declaring them here suppresses the read-before-write nil
|
|
80
|
+
# widening that would otherwise appear on those ivars in `it` / `specify`
|
|
81
|
+
# sibling blocks.
|
|
82
|
+
Rigor::Plugin::AdditionalInitializer.new(
|
|
83
|
+
receiver_constraint: "RSpec::ExampleGroup",
|
|
84
|
+
block_methods: %i[before let subject]
|
|
85
|
+
)
|
|
75
86
|
]
|
|
76
87
|
)
|
|
77
88
|
|
|
78
|
-
def init(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# Per-path `LetScopeIndex` cache. The plugin's
|
|
83
|
-
# `flow_contribution_for` is called for every call
|
|
84
|
-
# node the dispatcher visits; building the index once
|
|
89
|
+
def init(_services)
|
|
90
|
+
# Per-path `LetScopeIndex` cache. The let-binding
|
|
91
|
+
# `dynamic_return` rule (and its `file_methods:` gate)
|
|
92
|
+
# consult the index per call node; building it once
|
|
85
93
|
# per file is essential for performance.
|
|
86
94
|
@let_index_cache = {}
|
|
87
95
|
end
|
|
88
96
|
|
|
89
97
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
90
98
|
# Build the let-scope index for this file while we
|
|
91
|
-
# have the parsed root in hand —
|
|
99
|
+
# have the parsed root in hand — the let-binding rule
|
|
92
100
|
# picks it up from `@let_index_cache` keyed on path.
|
|
93
101
|
@let_index_cache[path] ||= LetScopeIndex.build(root)
|
|
94
102
|
Analyzer.diagnose(path: path, root: root).map { |diag| build_diagnostic(diag) }
|
|
@@ -101,23 +109,32 @@ module Rigor
|
|
|
101
109
|
MatcherAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
|
|
102
110
|
end
|
|
103
111
|
|
|
104
|
-
# Pillar 2 Slice 2 — binds local reads in `it` /
|
|
105
|
-
# their `let(:name) { ... }` block's inferred return
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
# Pillar 2 Slice 2 / ADR-52 slice 5a — binds local reads in `it` /
|
|
113
|
+
# spec bodies to their `let(:name) { ... }` block's inferred return
|
|
114
|
+
# type. The name set varies per file (each spec file's
|
|
115
|
+
# `describe`/`let` structure), so the rule gates on the per-file
|
|
116
|
+
# `file_methods:` form: the engine resolves the file's let names
|
|
117
|
+
# once per analysed file and consults the block only for a listed
|
|
118
|
+
# name; the line-scoped shadowing resolution stays in the block.
|
|
119
|
+
dynamic_return file_methods: ->(path) { let_names_for(path) } do |call_node, scope|
|
|
120
|
+
let_binding_return_type(call_node, scope)
|
|
112
121
|
end
|
|
113
122
|
|
|
114
123
|
private
|
|
115
124
|
|
|
125
|
+
# The `file_methods:` gate set — every `let` / `subject` name the
|
|
126
|
+
# file declares anywhere. A safe over-approximation of the block's
|
|
127
|
+
# own `let_block_at` line-scoped lookup (a name read outside its
|
|
128
|
+
# describe scope passes the gate and is declined by the block).
|
|
129
|
+
def let_names_for(path)
|
|
130
|
+
let_scope_index_for(path)&.let_names || []
|
|
131
|
+
end
|
|
132
|
+
|
|
116
133
|
# Pillar 2 Slice 2 — when the call node is a no-receiver
|
|
117
134
|
# method call (`user`, `subject`, etc.) inside an RSpec
|
|
118
135
|
# `describe` block whose lets include a matching name,
|
|
119
|
-
# return
|
|
120
|
-
def
|
|
136
|
+
# return the let block's inferred type.
|
|
137
|
+
def let_binding_return_type(call_node, scope)
|
|
121
138
|
return nil if scope.nil?
|
|
122
139
|
return nil unless candidate_call?(call_node)
|
|
123
140
|
|
|
@@ -129,15 +146,12 @@ module Rigor
|
|
|
129
146
|
return nil if block_node.nil?
|
|
130
147
|
|
|
131
148
|
describe_const = index.describe_const_at(line)
|
|
132
|
-
|
|
149
|
+
LetTypeResolver.resolve(
|
|
133
150
|
block_node,
|
|
134
151
|
describe_const: describe_const,
|
|
135
|
-
factory_index:
|
|
152
|
+
factory_index: read_fact(plugin_id: "factorybot", name: :factory_index),
|
|
136
153
|
environment: scope.environment
|
|
137
154
|
)
|
|
138
|
-
return nil if type.nil?
|
|
139
|
-
|
|
140
|
-
Rigor::FlowContribution.new(return_type: type)
|
|
141
155
|
end
|
|
142
156
|
|
|
143
157
|
def candidate_call?(call_node)
|
|
@@ -168,14 +182,6 @@ module Rigor
|
|
|
168
182
|
nil
|
|
169
183
|
end
|
|
170
184
|
|
|
171
|
-
def factory_index_or_nil
|
|
172
|
-
return @factory_index if @factory_index_resolved
|
|
173
|
-
|
|
174
|
-
@factory_index = @services&.fact_store&.read(plugin_id: "factorybot", name: :factory_index)
|
|
175
|
-
@factory_index_resolved = true
|
|
176
|
-
@factory_index
|
|
177
|
-
end
|
|
178
|
-
|
|
179
185
|
def build_diagnostic(diag)
|
|
180
186
|
Rigor::Analysis::Diagnostic.new(
|
|
181
187
|
path: diag.path, line: diag.line, column: diag.column,
|
|
@@ -70,19 +70,14 @@ module Rigor
|
|
|
70
70
|
]
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
-
def init(services)
|
|
74
|
-
@services = services
|
|
75
|
-
@model_index = nil
|
|
76
|
-
@model_index_resolved = false
|
|
77
|
-
end
|
|
78
|
-
|
|
79
73
|
# ADR-37 — per-matcher validation over the engine-owned walk. The
|
|
80
74
|
# model anchor (the enclosing `describe <Model>` const) comes from
|
|
81
75
|
# the node-rule NodeContext ancestors; the diagnostic points at the
|
|
82
76
|
# matcher name (message_loc). The :model_index fact (from
|
|
83
|
-
# rigor-activerecord) is read lazily
|
|
77
|
+
# rigor-activerecord) is read lazily via `read_fact`; without it
|
|
78
|
+
# the rule is silent.
|
|
84
79
|
node_rule Prism::CallNode do |node, _scope, path, _fc, context|
|
|
85
|
-
index =
|
|
80
|
+
index = read_fact(plugin_id: "activerecord", name: :model_index)
|
|
86
81
|
next [] if index.nil?
|
|
87
82
|
|
|
88
83
|
Analyzer.violations_for(matcher_call: node, ancestors: context.ancestors, model_index: index).map do |violation|
|
|
@@ -90,21 +85,6 @@ module Rigor
|
|
|
90
85
|
message: violation.message, severity: :warning, rule: violation.rule)
|
|
91
86
|
end
|
|
92
87
|
end
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
# Lazily resolves `:model_index` from
|
|
97
|
-
# `rigor-activerecord`. Returns nil when the plugin
|
|
98
|
-
# isn't loaded or no index has been published; the
|
|
99
|
-
# analyzer treats nil as "no cross-check available" and
|
|
100
|
-
# falls silent.
|
|
101
|
-
def model_index_or_nil
|
|
102
|
-
return @model_index if @model_index_resolved
|
|
103
|
-
|
|
104
|
-
@model_index = @services.fact_store.read(plugin_id: "activerecord", name: :model_index)
|
|
105
|
-
@model_index_resolved = true
|
|
106
|
-
@model_index
|
|
107
|
-
end
|
|
108
88
|
end
|
|
109
89
|
|
|
110
90
|
Rigor::Plugin.register(ShouldaMatchers)
|
|
@@ -63,7 +63,7 @@ module Rigor
|
|
|
63
63
|
}
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
producer :worker_index do |_params|
|
|
66
|
+
producer :worker_index, watch: -> { [[@worker_search_paths, "**/*.rb"]] } do |_params|
|
|
67
67
|
WorkerDiscoverer.new(
|
|
68
68
|
io_boundary: io_boundary,
|
|
69
69
|
search_paths: @worker_search_paths,
|
|
@@ -74,46 +74,33 @@ module Rigor
|
|
|
74
74
|
def init(_services)
|
|
75
75
|
@worker_search_paths = Array(config.fetch("worker_search_paths")).map(&:to_s)
|
|
76
76
|
@worker_marker_modules = Array(config.fetch("worker_marker_modules")).map(&:to_s)
|
|
77
|
-
@worker_index = nil
|
|
78
|
-
@load_error = nil
|
|
79
77
|
end
|
|
80
78
|
|
|
81
79
|
# File-level only: the load-error emission. The per-call arity
|
|
82
80
|
# validation runs over the engine-owned walk via the node_rule
|
|
83
81
|
# below (ADR-37). The worker index is lazily loaded + memoised by
|
|
84
|
-
#
|
|
82
|
+
# `producer_value`, shared by both surfaces.
|
|
85
83
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
86
|
-
index =
|
|
87
|
-
return [load_error_diagnostic(path)] if index.nil? &&
|
|
84
|
+
index = producer_value(:worker_index)
|
|
85
|
+
return [load_error_diagnostic(path)] if index.nil? && producer_error(:worker_index)
|
|
88
86
|
|
|
89
87
|
[]
|
|
90
88
|
end
|
|
91
89
|
|
|
92
90
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
93
|
-
index =
|
|
91
|
+
index = producer_value(:worker_index)
|
|
94
92
|
next [] if index.nil? || index.empty?
|
|
95
93
|
|
|
96
|
-
Analyzer.violations_for(call_node: node, worker_index: index)
|
|
97
|
-
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
98
|
-
end
|
|
94
|
+
diagnostics_for(Analyzer.violations_for(call_node: node, worker_index: index), path: path, node: node)
|
|
99
95
|
end
|
|
100
96
|
|
|
101
97
|
private
|
|
102
98
|
|
|
103
|
-
def worker_index_or_nil
|
|
104
|
-
return @worker_index if @worker_index
|
|
105
|
-
|
|
106
|
-
descriptor = glob_descriptor(@worker_search_paths, "**/*.rb")
|
|
107
|
-
@worker_index = cache_for(:worker_index, params: {}, descriptor: descriptor).call
|
|
108
|
-
rescue StandardError => e
|
|
109
|
-
@load_error = "rigor-sidekiq: failed to discover workers: #{e.class}: #{e.message}"
|
|
110
|
-
nil
|
|
111
|
-
end
|
|
112
|
-
|
|
113
99
|
def load_error_diagnostic(path)
|
|
100
|
+
error = producer_error(:worker_index)
|
|
114
101
|
Rigor::Analysis::Diagnostic.new(
|
|
115
102
|
path: path, line: 1, column: 1,
|
|
116
|
-
message:
|
|
103
|
+
message: "rigor-sidekiq: failed to discover workers: #{error.class}: #{error.message}",
|
|
117
104
|
severity: :warning,
|
|
118
105
|
rule: "load-error"
|
|
119
106
|
)
|
|
@@ -74,7 +74,7 @@ module Rigor
|
|
|
74
74
|
block_as_methods: [
|
|
75
75
|
Rigor::Plugin::Macro::BlockAsMethod.new(
|
|
76
76
|
receiver_constraint: "Sinatra::Base",
|
|
77
|
-
|
|
77
|
+
method_names: %i[get post put delete head options patch link unlink]
|
|
78
78
|
)
|
|
79
79
|
]
|
|
80
80
|
)
|
|
@@ -28,16 +28,15 @@ module Rigor
|
|
|
28
28
|
#
|
|
29
29
|
# ## Two-phase mechanism
|
|
30
30
|
#
|
|
31
|
-
# The recogniser is invoked from `
|
|
31
|
+
# The recogniser is invoked from the plugin's `dynamic_return` rule
|
|
32
32
|
# where the per-node `scope:` carries the proper narrowing
|
|
33
|
-
# context.
|
|
33
|
+
# context. The rule:
|
|
34
34
|
#
|
|
35
|
-
# -
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# what users of Sorbet expect.
|
|
35
|
+
# - Contributes a `bot` return type regardless of
|
|
36
|
+
# reachability (faithful to `T.absurd`'s runtime
|
|
37
|
+
# behaviour: it always raises). This lets the engine's
|
|
38
|
+
# existing flow analysis treat code after `T.absurd` as
|
|
39
|
+
# unreachable, matching what users of Sorbet expect.
|
|
41
40
|
# - When the branch is REACHABLE (the discriminant's type
|
|
42
41
|
# isn't `bot`), the recogniser also records the call
|
|
43
42
|
# node in a per-plugin set. The plugin's
|
|
@@ -47,7 +46,7 @@ module Rigor
|
|
|
47
46
|
# call_node whose object identity matches the recorded
|
|
48
47
|
# set. We rely on the runner only parsing each file
|
|
49
48
|
# once per run, so the same Prism node object is seen
|
|
50
|
-
# in both `
|
|
49
|
+
# in both the `dynamic_return` rule and
|
|
51
50
|
# `diagnostics_for_file`.
|
|
52
51
|
module AbsurdRecognizer
|
|
53
52
|
# @param call_node [Prism::CallNode]
|
|
@@ -82,26 +81,6 @@ module Rigor
|
|
|
82
81
|
# diagnostic fires conservatively.
|
|
83
82
|
false
|
|
84
83
|
end
|
|
85
|
-
|
|
86
|
-
# The contribution every `T.absurd` call gets,
|
|
87
|
-
# regardless of static reachability — `T.absurd` raises
|
|
88
|
-
# at runtime, so its return type is `bot` and the call
|
|
89
|
-
# is exceptional. This lets the engine's flow analysis
|
|
90
|
-
# treat code after the call as unreachable (no
|
|
91
|
-
# `flow.unreachable-branch` from us; that's an engine
|
|
92
|
-
# rule that consults the same effect lattice).
|
|
93
|
-
def self.contribution(call_node, plugin_id)
|
|
94
|
-
Rigor::FlowContribution.new(
|
|
95
|
-
return_type: Rigor::Type::Combinator.bot,
|
|
96
|
-
exceptional: :raises,
|
|
97
|
-
provenance: Rigor::FlowContribution::Provenance.new(
|
|
98
|
-
source_family: "plugin.#{plugin_id}",
|
|
99
|
-
plugin_id: plugin_id,
|
|
100
|
-
node: call_node,
|
|
101
|
-
descriptor: nil
|
|
102
|
-
)
|
|
103
|
-
)
|
|
104
|
-
end
|
|
105
84
|
end
|
|
106
85
|
end
|
|
107
86
|
end
|
|
@@ -6,7 +6,7 @@ module Rigor
|
|
|
6
6
|
# Per-run table of method signatures keyed by the
|
|
7
7
|
# `(class_name, method_name, kind)` triple. Built by
|
|
8
8
|
# {CatalogWalker} during the plugin's lazy pre-walk; read
|
|
9
|
-
# by
|
|
9
|
+
# by the plugin's `dynamic_return` rule at every gated call site.
|
|
10
10
|
#
|
|
11
11
|
# The catalog is mutable while it is being built, then
|
|
12
12
|
# frozen via {#freeze!} before the first read. Construction
|
|
@@ -80,6 +80,22 @@ module Rigor
|
|
|
80
80
|
@entries.empty?
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# ADR-52 slice 4 — the distinct method names the catalog
|
|
84
|
+
# carries at least one signature for, across every
|
|
85
|
+
# `(class_name, kind)` owner. Feeds the plugin's run-time
|
|
86
|
+
# `dynamic_return methods:` name gate: the engine only
|
|
87
|
+
# consults the plugin for a call whose name appears here
|
|
88
|
+
# (or in the static assertion vocabulary), and the
|
|
89
|
+
# precise `(class, kind)` lookup stays in the rule block.
|
|
90
|
+
# Computed fresh per call — the plugin memoises the
|
|
91
|
+
# resolved set, and `freeze!` freezes the catalog itself
|
|
92
|
+
# so a lazy memo ivar here would raise.
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<Symbol>]
|
|
95
|
+
def method_names
|
|
96
|
+
@entries.keys.map { |key| key[1] }.uniq
|
|
97
|
+
end
|
|
98
|
+
|
|
83
99
|
def size
|
|
84
100
|
@entries.size
|
|
85
101
|
end
|
|
@@ -24,8 +24,8 @@ module Rigor
|
|
|
24
24
|
# identically — per-call-site sigil honouring (e.g. only
|
|
25
25
|
# firing `T.let` recognition in `# typed: true`+ files)
|
|
26
26
|
# requires threading the file path through
|
|
27
|
-
#
|
|
28
|
-
# plugin-contract widening slice.
|
|
27
|
+
# the per-call recognition path, which lives behind a
|
|
28
|
+
# future plugin-contract widening slice.
|
|
29
29
|
module SigilDetector
|
|
30
30
|
# Sorbet's strictness-level names. Stored as symbols to
|
|
31
31
|
# match the analyzer's existing convention for level
|