rigortype 0.1.18 → 0.2.0
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 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +74 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +1072 -71
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- 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 +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- 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 +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
- 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 +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- 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/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -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 +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -66,7 +66,7 @@ module Rigor
|
|
|
66
66
|
}
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
-
producer :policy_index do |_params|
|
|
69
|
+
producer :policy_index, watch: -> { [[@policy_search_paths, "**/*.rb"]] } do |_params|
|
|
70
70
|
PolicyDiscoverer.new(
|
|
71
71
|
io_boundary: io_boundary,
|
|
72
72
|
search_paths: @policy_search_paths,
|
|
@@ -77,46 +77,35 @@ module Rigor
|
|
|
77
77
|
def init(_services)
|
|
78
78
|
@policy_search_paths = Array(config.fetch("policy_search_paths")).map(&:to_s)
|
|
79
79
|
@policy_base_classes = Array(config.fetch("policy_base_classes")).map(&:to_s)
|
|
80
|
-
@policy_index = nil
|
|
81
|
-
@load_error = nil
|
|
82
80
|
end
|
|
83
81
|
|
|
84
82
|
# File-level only: the load-error emission. The per-call policy
|
|
85
83
|
# validation runs over the engine-owned walk via the node_rule
|
|
86
84
|
# below (ADR-37). The index is lazily loaded + memoised by
|
|
87
|
-
#
|
|
85
|
+
# `producer_value`, so both surfaces share one load.
|
|
88
86
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
89
|
-
index =
|
|
90
|
-
return [load_error_diagnostic(path)] if index.nil? &&
|
|
87
|
+
index = producer_value(:policy_index)
|
|
88
|
+
return [load_error_diagnostic(path)] if index.nil? && producer_error(:policy_index)
|
|
91
89
|
|
|
92
90
|
[]
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
node_rule Prism::CallNode do |node, scope, path|
|
|
96
|
-
index =
|
|
94
|
+
index = producer_value(:policy_index)
|
|
97
95
|
next [] if index.nil? || index.empty?
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
diagnostics_for(
|
|
98
|
+
Analyzer.violations_for(call_node: node, policy_index: index, scope: scope), path: path, node: node
|
|
99
|
+
)
|
|
102
100
|
end
|
|
103
101
|
|
|
104
102
|
private
|
|
105
103
|
|
|
106
|
-
def policy_index_or_nil
|
|
107
|
-
return @policy_index if @policy_index
|
|
108
|
-
|
|
109
|
-
descriptor = glob_descriptor(@policy_search_paths, "**/*.rb")
|
|
110
|
-
@policy_index = cache_for(:policy_index, params: {}, descriptor: descriptor).call
|
|
111
|
-
rescue StandardError => e
|
|
112
|
-
@load_error = "rigor-pundit: failed to discover policies: #{e.class}: #{e.message}"
|
|
113
|
-
nil
|
|
114
|
-
end
|
|
115
|
-
|
|
116
104
|
def load_error_diagnostic(path)
|
|
105
|
+
error = producer_error(:policy_index)
|
|
117
106
|
Rigor::Analysis::Diagnostic.new(
|
|
118
107
|
path: path, line: 1, column: 1,
|
|
119
|
-
message:
|
|
108
|
+
message: "rigor-pundit: failed to discover policies: #{error.class}: #{error.message}",
|
|
120
109
|
severity: :warning,
|
|
121
110
|
rule: "load-error"
|
|
122
111
|
)
|
|
@@ -169,7 +169,7 @@ module Rigor
|
|
|
169
169
|
|
|
170
170
|
# Extracts the literal-string first argument when
|
|
171
171
|
# present. Returns nil for variable / expression keys —
|
|
172
|
-
#
|
|
172
|
+
# only literal keys are statically validated.
|
|
173
173
|
def literal_key_for(call_node)
|
|
174
174
|
args = call_node.arguments&.arguments || []
|
|
175
175
|
return nil if args.empty?
|
|
@@ -224,9 +224,7 @@ module Rigor
|
|
|
224
224
|
|
|
225
225
|
def collect_assoc_keys(hash_node)
|
|
226
226
|
# Both `Prism::HashNode` and `Prism::KeywordHashNode`
|
|
227
|
-
# expose `#elements
|
|
228
|
-
# accidental no-op carried over from an earlier
|
|
229
|
-
# draft.
|
|
227
|
+
# expose `#elements`, so a single path handles both.
|
|
230
228
|
hash_node.elements.filter_map do |element|
|
|
231
229
|
next nil unless element.is_a?(Prism::AssocNode)
|
|
232
230
|
|
|
@@ -57,7 +57,7 @@ module Rigor
|
|
|
57
57
|
parsed.each do |locale, tree|
|
|
58
58
|
locale = locale.to_s
|
|
59
59
|
locales << locale
|
|
60
|
-
|
|
60
|
+
each_flattened(tree, []) do |dotted_key, value|
|
|
61
61
|
placeholders = (per_key[dotted_key] ||= {})
|
|
62
62
|
placeholders[locale] = extract_placeholders(value)
|
|
63
63
|
kinds = (per_key_kinds[dotted_key] ||= {})
|
|
@@ -101,24 +101,40 @@ module Rigor
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Recursively walks the per-locale subtree, yielding
|
|
104
|
-
# `[dotted_key, leaf_value]`
|
|
104
|
+
# `[dotted_key, leaf_value]` for each leaf. Hash leaves are
|
|
105
105
|
# *not* recorded as entries themselves — only their
|
|
106
|
-
# descendants — but every leaf scalar / array IS
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
# descendants — but every leaf scalar / array IS recorded.
|
|
107
|
+
#
|
|
108
|
+
# `breadcrumbs` is a single mutable stack reused across the
|
|
109
|
+
# whole walk (push before recursing, pop after): for the
|
|
110
|
+
# 530-file / 14 MB Mastodon locale corpus the old
|
|
111
|
+
# `flat_map { flatten_tree(v, breadcrumbs + [k]) }` shape
|
|
112
|
+
# allocated a fresh breadcrumb Array at every node plus an
|
|
113
|
+
# intermediate result Array at every level — millions of
|
|
114
|
+
# short-lived objects and the run's top allocation site. The
|
|
115
|
+
# dotted key is still materialised once per leaf (it has to
|
|
116
|
+
# be); everything else is now allocation-free traversal.
|
|
117
|
+
def each_flattened(node, breadcrumbs, &)
|
|
118
|
+
if node.is_a?(Hash)
|
|
119
|
+
node.each do |k, v|
|
|
120
|
+
breadcrumbs.push(k.to_s)
|
|
121
|
+
each_flattened(v, breadcrumbs, &)
|
|
122
|
+
breadcrumbs.pop
|
|
113
123
|
end
|
|
114
124
|
else
|
|
115
|
-
|
|
125
|
+
yield breadcrumbs.join("."), node
|
|
116
126
|
end
|
|
117
127
|
end
|
|
118
128
|
|
|
119
129
|
def extract_placeholders(value)
|
|
120
130
|
case value
|
|
121
|
-
when String
|
|
131
|
+
when String
|
|
132
|
+
# Most locale leaves carry no `%{var}`; skip the scan +
|
|
133
|
+
# flatten + to_set allocation trio for them. A string with
|
|
134
|
+
# no `%{` yields an empty placeholder set either way.
|
|
135
|
+
return Set.new unless value.include?("%{")
|
|
136
|
+
|
|
137
|
+
value.scan(PLACEHOLDER_RE).flatten.to_set
|
|
122
138
|
when Array then value.map { |v| extract_placeholders(v) }.reduce(Set.new) { |a, s| a | s }
|
|
123
139
|
else Set.new
|
|
124
140
|
end
|
|
@@ -40,7 +40,7 @@ module Rigor
|
|
|
40
40
|
# arguments. Missing placeholders are errors; extra
|
|
41
41
|
# arguments are warnings.
|
|
42
42
|
#
|
|
43
|
-
# ## Limitations
|
|
43
|
+
# ## Limitations
|
|
44
44
|
#
|
|
45
45
|
# - Only literal-string keys are validated. `t(key)` with
|
|
46
46
|
# a variable receiver is silently passed through.
|
|
@@ -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
|
)
|
|
@@ -38,11 +38,9 @@ module Rigor
|
|
|
38
38
|
# (`user_facebook_omniauth_authorize_path`) and Devise
|
|
39
39
|
# declares them from the configured providers, which
|
|
40
40
|
# live in an initializer this static parser does not
|
|
41
|
-
# read.
|
|
42
|
-
# `
|
|
43
|
-
#
|
|
44
|
-
# these are NOT in the table but consulted by the
|
|
45
|
-
# `Analyzer.allowed_dynamic_pattern?` check.
|
|
41
|
+
# read. The suffix patterns are registered in
|
|
42
|
+
# `OMNIAUTH_SUFFIXES` and consulted by
|
|
43
|
+
# `HelperTable#omniauth_match?`.
|
|
46
44
|
module DeviseRoutes
|
|
47
45
|
# The standard Devise controllers and the helper
|
|
48
46
|
# actions each generates. Keys are the controller
|
|
@@ -190,7 +188,7 @@ module Rigor
|
|
|
190
188
|
|
|
191
189
|
# Returns the set of OmniAuth pattern suffixes the
|
|
192
190
|
# analyzer accepts for a given resource. The analyzer
|
|
193
|
-
# consults this set (via `HelperTable#
|
|
191
|
+
# consults this set (via `HelperTable#omniauth_match?`)
|
|
194
192
|
# when a `*_path` / `*_url` call's name does not match
|
|
195
193
|
# any registered entry and its prefix matches a Devise
|
|
196
194
|
# resource.
|
|
@@ -1171,9 +1171,10 @@ module Rigor
|
|
|
1171
1171
|
end
|
|
1172
1172
|
|
|
1173
1173
|
def in_singular_resource?(*)
|
|
1174
|
-
#
|
|
1175
|
-
#
|
|
1176
|
-
#
|
|
1174
|
+
# Stub: always returns true so member / collection
|
|
1175
|
+
# blocks descend. The singular-resource frame
|
|
1176
|
+
# (`push_singular_resource`) is modelled in Context;
|
|
1177
|
+
# a future caller could use it to tighten this check.
|
|
1177
1178
|
true
|
|
1178
1179
|
end
|
|
1179
1180
|
|
|
@@ -1181,21 +1182,14 @@ module Rigor
|
|
|
1181
1182
|
# `plural: true` for `resources :users`, `false` for
|
|
1182
1183
|
# `resource :profile`.
|
|
1183
1184
|
def register_resourceful_helpers(name, actions, base_arity, context, plural:)
|
|
1184
|
-
#
|
|
1185
|
-
#
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1190
|
-
# `relationship_path
|
|
1191
|
-
#
|
|
1192
|
-
# Plural resources singularise for show / new / edit
|
|
1193
|
-
# helpers (`resources :users` → `user_path(id)`);
|
|
1194
|
-
# singular resources use the name AS-IS even when it
|
|
1195
|
-
# looks plural (Mastodon's `resource :relationships,
|
|
1196
|
-
# only: [:show, :update]` → `relationships_path`).
|
|
1197
|
-
# The path segment uses `name` in both shapes — Rails
|
|
1198
|
-
# never singularises the URL.
|
|
1185
|
+
# Plural resources (`resources :users`) singularise
|
|
1186
|
+
# for show / new / edit helpers → `user_path(id)`.
|
|
1187
|
+
# Singular resources (`resource :foo`) use the name
|
|
1188
|
+
# AS-IS — singularising would mangle deliberately-
|
|
1189
|
+
# plural names like Mastodon's `resource
|
|
1190
|
+
# :relationships` → `relationships_path`, not
|
|
1191
|
+
# `relationship_path`. The URL path uses `name`
|
|
1192
|
+
# in both shapes (Rails never singularises the URL).
|
|
1199
1193
|
singular = plural ? singularize_word(name.to_s) : name.to_s
|
|
1200
1194
|
path_base = "#{context.path_prefix}/#{name}"
|
|
1201
1195
|
|
|
@@ -26,9 +26,9 @@ module Rigor
|
|
|
26
26
|
# - One level of nested `resources`
|
|
27
27
|
#
|
|
28
28
|
# The plugin publishes its parsed `:helper_table` through
|
|
29
|
-
# the ADR-9 cross-plugin fact store
|
|
30
|
-
#
|
|
31
|
-
#
|
|
29
|
+
# the ADR-9 cross-plugin fact store; `rigor-actionpack`
|
|
30
|
+
# Phase 4 consumes it for route-helper validation in
|
|
31
|
+
# controller code.
|
|
32
32
|
#
|
|
33
33
|
# ## Configuration
|
|
34
34
|
#
|
|
@@ -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)
|
|
@@ -150,8 +147,8 @@ module Rigor
|
|
|
150
147
|
end
|
|
151
148
|
|
|
152
149
|
# Publishes the parsed table to the cross-plugin fact
|
|
153
|
-
# store
|
|
154
|
-
#
|
|
150
|
+
# store; `rigor-actionpack` Phase 4 reads it via
|
|
151
|
+
# `services.fact_store.read`.
|
|
155
152
|
def prepare(services)
|
|
156
153
|
table = helper_table_or_nil
|
|
157
154
|
return if table.nil?
|
|
@@ -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,10 +21,9 @@ module Rigor
|
|
|
21
21
|
# { ... }` and `subject(:name) { ... }` declarations.
|
|
22
22
|
# `:subject` is the key for the implicit `subject { ... }`.
|
|
23
23
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# block's inferred type.
|
|
24
|
+
# Used by the plugin's let-binding `dynamic_return`
|
|
25
|
+
# rule to bind `let`-named method-shape calls inside
|
|
26
|
+
# `it` bodies to the let block's inferred type.
|
|
28
27
|
class LetScopeIndex
|
|
29
28
|
Record = Struct.new(:outer_range, :describe_const, :lets, keyword_init: true) do
|
|
30
29
|
def contains?(line) = outer_range.cover?(line)
|
|
@@ -6,7 +6,7 @@ require "rigor/type"
|
|
|
6
6
|
module Rigor
|
|
7
7
|
module Plugin
|
|
8
8
|
class Rspec < Rigor::Plugin::Base
|
|
9
|
-
#
|
|
9
|
+
# Resolves the runtime type of a
|
|
10
10
|
# `let(:name) { body }` or `subject(:name) { body }`
|
|
11
11
|
# block by pattern-matching its body's tail expression.
|
|
12
12
|
#
|
|
@@ -8,7 +8,7 @@ require "rigor/flow_contribution/fact"
|
|
|
8
8
|
module Rigor
|
|
9
9
|
module Plugin
|
|
10
10
|
class Rspec < Rigor::Plugin::Base
|
|
11
|
-
#
|
|
11
|
+
# Recognises `expect(x).to MATCHER`
|
|
12
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.
|
|
@@ -11,15 +11,13 @@ require_relative "rspec/let_type_resolver"
|
|
|
11
11
|
module Rigor
|
|
12
12
|
module Plugin
|
|
13
13
|
# rigor-rspec — validates RSpec `let` / `subject`
|
|
14
|
-
# declarations within each describe / context scope
|
|
14
|
+
# declarations within each describe / context scope,
|
|
15
|
+
# narrows `expect(x).to MATCHER` assertions downstream,
|
|
16
|
+
# and binds `let`/`subject` locals to their inferred
|
|
17
|
+
# return types inside `it` bodies.
|
|
15
18
|
#
|
|
16
19
|
# Tier 3A of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
|
|
17
|
-
#
|
|
18
|
-
# larger plugin (let-typo detection in `it` bodies,
|
|
19
|
-
# `expect(x).to receive(:method)` mock-target
|
|
20
|
-
# validation). Both are out of scope for v0.1.0; this
|
|
21
|
-
# plugin ships the two checks that have the lowest
|
|
22
|
-
# false-positive risk:
|
|
20
|
+
# Ships four checks:
|
|
23
21
|
#
|
|
24
22
|
# 1. **Duplicate `let` / `subject` declarations** in
|
|
25
23
|
# the same scope (`warning`). RSpec's runtime lets
|
|
@@ -31,14 +29,21 @@ module Rigor
|
|
|
31
29
|
# (`error`). At runtime this infinite-loops; users
|
|
32
30
|
# typically meant to call a different method or
|
|
33
31
|
# forgot to introduce a `super`.
|
|
32
|
+
# 3. **`expect(x).to MATCHER` narrowing** — narrows
|
|
33
|
+
# the named local `x` on the post-call edge for
|
|
34
|
+
# eight matchers (see `MatcherAnalyzer`).
|
|
35
|
+
# 4. **`let`/`subject` local binding** — binds `let`
|
|
36
|
+
# / `subject` names in `it` bodies to the block's
|
|
37
|
+
# inferred return type (see `LetTypeResolver`).
|
|
34
38
|
#
|
|
35
39
|
# ## Configuration
|
|
36
40
|
#
|
|
37
|
-
# No knobs
|
|
38
|
-
# file
|
|
39
|
-
# files outside the project's `paths:` are not
|
|
41
|
+
# No configuration knobs. The plugin walks every
|
|
42
|
+
# analysed file for `RSpec.describe ... do` blocks;
|
|
43
|
+
# spec files outside the project's `paths:` are not
|
|
44
|
+
# scanned.
|
|
40
45
|
#
|
|
41
|
-
# ## Limitations
|
|
46
|
+
# ## Limitations
|
|
42
47
|
#
|
|
43
48
|
# - **No let-typo detection.** Detecting an `it`
|
|
44
49
|
# block's reference to a misspelled `let` name
|
|
@@ -86,10 +91,7 @@ module Rigor
|
|
|
86
91
|
]
|
|
87
92
|
)
|
|
88
93
|
|
|
89
|
-
def init(
|
|
90
|
-
@services = services
|
|
91
|
-
@factory_index_resolved = false
|
|
92
|
-
@factory_index = nil
|
|
94
|
+
def init(_services)
|
|
93
95
|
# Per-path `LetScopeIndex` cache. The let-binding
|
|
94
96
|
# `dynamic_return` rule (and its `file_methods:` gate)
|
|
95
97
|
# consult the index per call node; building it once
|
|
@@ -105,14 +107,14 @@ module Rigor
|
|
|
105
107
|
Analyzer.diagnose(path: path, root: root).map { |diag| build_diagnostic(diag) }
|
|
106
108
|
end
|
|
107
109
|
|
|
108
|
-
# ADR-37 slice 2 —
|
|
110
|
+
# ADR-37 slice 2 — matcher narrowing
|
|
109
111
|
# (`expect(x).to be_a(T)` → `post_return_facts` on `x`),
|
|
110
112
|
# method-gated by the engine on the expectation verbs.
|
|
111
113
|
type_specifier methods: %i[to not_to to_not] do |call_node, scope|
|
|
112
114
|
MatcherAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
|
|
113
115
|
end
|
|
114
116
|
|
|
115
|
-
#
|
|
117
|
+
# ADR-52 slice 5a — binds local reads in `it` /
|
|
116
118
|
# spec bodies to their `let(:name) { ... }` block's inferred return
|
|
117
119
|
# type. The name set varies per file (each spec file's
|
|
118
120
|
# `describe`/`let` structure), so the rule gates on the per-file
|
|
@@ -133,7 +135,7 @@ module Rigor
|
|
|
133
135
|
let_scope_index_for(path)&.let_names || []
|
|
134
136
|
end
|
|
135
137
|
|
|
136
|
-
#
|
|
138
|
+
# When the call node is a no-receiver
|
|
137
139
|
# method call (`user`, `subject`, etc.) inside an RSpec
|
|
138
140
|
# `describe` block whose lets include a matching name,
|
|
139
141
|
# return the let block's inferred type.
|
|
@@ -152,7 +154,7 @@ module Rigor
|
|
|
152
154
|
LetTypeResolver.resolve(
|
|
153
155
|
block_node,
|
|
154
156
|
describe_const: describe_const,
|
|
155
|
-
factory_index:
|
|
157
|
+
factory_index: read_fact(plugin_id: "factorybot", name: :factory_index),
|
|
156
158
|
environment: scope.environment
|
|
157
159
|
)
|
|
158
160
|
end
|
|
@@ -185,14 +187,6 @@ module Rigor
|
|
|
185
187
|
nil
|
|
186
188
|
end
|
|
187
189
|
|
|
188
|
-
def factory_index_or_nil
|
|
189
|
-
return @factory_index if @factory_index_resolved
|
|
190
|
-
|
|
191
|
-
@factory_index = @services&.fact_store&.read(plugin_id: "factorybot", name: :factory_index)
|
|
192
|
-
@factory_index_resolved = true
|
|
193
|
-
@factory_index
|
|
194
|
-
end
|
|
195
|
-
|
|
196
190
|
def build_diagnostic(diag)
|
|
197
191
|
Rigor::Analysis::Diagnostic.new(
|
|
198
192
|
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)
|
|
@@ -9,10 +9,11 @@ module Rigor
|
|
|
9
9
|
# analyzer can validate `Worker.perform_async(...)`
|
|
10
10
|
# call sites.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
12
|
+
# Uses the same `min_arity` / `max_arity` closed-range
|
|
13
|
+
# envelope as `rigor-activejob`'s `JobIndex::Entry`
|
|
14
|
+
# (`Float::INFINITY` upper bound when `*args` is present);
|
|
15
|
+
# Sidekiq workers serialize args to JSON so keyword arity
|
|
16
|
+
# is not tracked here.
|
|
16
17
|
class WorkerIndex
|
|
17
18
|
Entry = Data.define(:class_name, :min_arity, :max_arity) do
|
|
18
19
|
def arity_label
|