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
|
@@ -69,9 +69,9 @@ module Rigor
|
|
|
69
69
|
# True when at least one discovered channel uses a
|
|
70
70
|
# dynamic stream registration. The analyzer treats
|
|
71
71
|
# this as "we can't be sure any literal name is
|
|
72
|
-
# missing" and
|
|
73
|
-
#
|
|
74
|
-
#
|
|
72
|
+
# missing" and skips the `unknown-stream` warning
|
|
73
|
+
# entirely — absence of a literal match doesn't prove
|
|
74
|
+
# the name is wrong.
|
|
75
75
|
def any_dynamic_streams?
|
|
76
76
|
@entries.any?(&:dynamic_streams)
|
|
77
77
|
end
|
|
@@ -41,7 +41,7 @@ module Rigor
|
|
|
41
41
|
# `stream_for record`) — the absence of a literal
|
|
42
42
|
# match doesn't prove absence.
|
|
43
43
|
#
|
|
44
|
-
# ## Limitations
|
|
44
|
+
# ## Limitations
|
|
45
45
|
#
|
|
46
46
|
# - **Direct-superclass match only.** Indirect
|
|
47
47
|
# inheritance (`AdminChannel < BaseChannel <
|
|
@@ -51,8 +51,8 @@ module Rigor
|
|
|
51
51
|
# ActionCable actions are invoked from JS via
|
|
52
52
|
# `subscription.perform("action_name", data)`; we
|
|
53
53
|
# don't analyse JS so the action-method index is
|
|
54
|
-
#
|
|
55
|
-
#
|
|
54
|
+
# informational only (deferred: cross-plugin handoff
|
|
55
|
+
# to a JS-side analyzer).
|
|
56
56
|
# - **`broadcast_to` arity isn't checked.** The method
|
|
57
57
|
# takes any record + any data hash; there's no
|
|
58
58
|
# useful arity envelope.
|
|
@@ -70,7 +70,12 @@ module Rigor
|
|
|
70
70
|
}
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
# `watch:` covers every `.rb` file under the channel search paths
|
|
74
|
+
# so the cache invalidates when channels are added, removed, or
|
|
75
|
+
# edited (ADR-60 WD3). The dependency descriptor is recorded after
|
|
76
|
+
# the discoverer runs, so the `io_boundary` reads inside it are
|
|
77
|
+
# captured too.
|
|
78
|
+
producer :channel_index, watch: -> { [[@channel_search_paths, "**/*.rb"]] } do |_params|
|
|
74
79
|
ChannelDiscoverer.new(
|
|
75
80
|
io_boundary: io_boundary,
|
|
76
81
|
search_paths: @channel_search_paths,
|
|
@@ -81,54 +86,33 @@ module Rigor
|
|
|
81
86
|
def init(_services)
|
|
82
87
|
@channel_search_paths = Array(config.fetch("channel_search_paths")).map(&:to_s)
|
|
83
88
|
@channel_base_classes = Array(config.fetch("channel_base_classes")).map(&:to_s)
|
|
84
|
-
@channel_index = nil
|
|
85
|
-
@load_error = nil
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
# File-level only: the load-error emission. Per-call broadcast
|
|
89
92
|
# validation runs over the engine-owned walk via the node_rule
|
|
90
93
|
# below (ADR-37). The channel index is lazily loaded + memoised by
|
|
91
|
-
#
|
|
94
|
+
# `producer_value`, shared by both surfaces.
|
|
92
95
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
93
|
-
index =
|
|
94
|
-
return [load_error_diagnostic(path)] if index.nil? &&
|
|
96
|
+
index = producer_value(:channel_index)
|
|
97
|
+
return [load_error_diagnostic(path)] if index.nil? && producer_error(:channel_index)
|
|
95
98
|
|
|
96
99
|
[]
|
|
97
100
|
end
|
|
98
101
|
|
|
99
102
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
100
|
-
index =
|
|
103
|
+
index = producer_value(:channel_index)
|
|
101
104
|
next [] if index.nil? || index.empty?
|
|
102
105
|
|
|
103
|
-
Analyzer.violations_for(call_node: node, channel_index: index)
|
|
104
|
-
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
105
|
-
end
|
|
106
|
+
diagnostics_for(Analyzer.violations_for(call_node: node, channel_index: index), path: path, node: node)
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
private
|
|
109
110
|
|
|
110
|
-
def channel_index_or_nil
|
|
111
|
-
return @channel_index if @channel_index
|
|
112
|
-
|
|
113
|
-
# Pass an explicit descriptor covering every `.rb` file
|
|
114
|
-
# under the configured channel search paths so the cache
|
|
115
|
-
# invalidates when channels are added, removed, or edited.
|
|
116
|
-
# Without it the auto-built descriptor depends on the
|
|
117
|
-
# `IoBoundary`'s in-process read history — empty on the
|
|
118
|
-
# first call of a fresh process, so warm cache hits would
|
|
119
|
-
# serve stale `ChannelIndex` data when project files have
|
|
120
|
-
# changed between sessions.
|
|
121
|
-
descriptor = glob_descriptor(@channel_search_paths, "**/*.rb")
|
|
122
|
-
@channel_index = cache_for(:channel_index, params: {}, descriptor: descriptor).call
|
|
123
|
-
rescue StandardError => e
|
|
124
|
-
@load_error = "rigor-actioncable: failed to discover channels: #{e.class}: #{e.message}"
|
|
125
|
-
nil
|
|
126
|
-
end
|
|
127
|
-
|
|
128
111
|
def load_error_diagnostic(path)
|
|
112
|
+
error = producer_error(:channel_index)
|
|
129
113
|
Rigor::Analysis::Diagnostic.new(
|
|
130
114
|
path: path, line: 1, column: 1,
|
|
131
|
-
message:
|
|
115
|
+
message: "rigor-actioncable: failed to discover channels: #{error.class}: #{error.message}",
|
|
132
116
|
severity: :warning,
|
|
133
117
|
rule: "load-error"
|
|
134
118
|
)
|
|
@@ -301,19 +301,11 @@ module Rigor
|
|
|
301
301
|
[entry.method_name, entry]
|
|
302
302
|
end
|
|
303
303
|
|
|
304
|
-
# Merge
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
# the class's lexical chain looking for a nested
|
|
310
|
-
# match (e.g. `Emails::Issues` inside `class Notify`
|
|
311
|
-
# at top-level resolves to top-level `Emails::Issues`).
|
|
312
|
-
# Includes we cannot resolve are silently skipped;
|
|
313
|
-
# the per-mailer `unresolved_includes?` predicate
|
|
314
|
-
# below (consumed by the analyzer) downgrades
|
|
315
|
-
# `unknown-action` to silence when any include is
|
|
316
|
-
# unresolved.
|
|
304
|
+
# Merge actions from include'd modules (pre-collected
|
|
305
|
+
# in `module_actions` keyed by fully-qualified name).
|
|
306
|
+
# Unresolvable includes are tracked; `unresolved_includes?`
|
|
307
|
+
# (consumed by the analyzer) downgrades `unknown-action`
|
|
308
|
+
# to silence when any include remains unresolved.
|
|
317
309
|
unresolved_includes = []
|
|
318
310
|
includes.each do |include_name|
|
|
319
311
|
inc_actions = module_actions[include_name]
|
|
@@ -65,7 +65,13 @@ module Rigor
|
|
|
65
65
|
}
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
# `watch:` covers every mailer class under `mailer_search_paths`
|
|
69
|
+
# AND every view template under `views_root` (ADR-60 WD3) — a
|
|
70
|
+
# newly-added view a mailer references must invalidate the index,
|
|
71
|
+
# and `view_exists?` failures the producer never records would
|
|
72
|
+
# otherwise be invisible to the dependency descriptor.
|
|
73
|
+
producer :mailer_index,
|
|
74
|
+
watch: -> { [[@mailer_search_paths, "**/*.rb"], [@views_root, "**/*"]] } do |_params|
|
|
69
75
|
MailerDiscoverer.new(
|
|
70
76
|
io_boundary: io_boundary,
|
|
71
77
|
search_paths: @mailer_search_paths,
|
|
@@ -78,8 +84,6 @@ module Rigor
|
|
|
78
84
|
@mailer_search_paths = Array(config.fetch("mailer_search_paths")).map(&:to_s)
|
|
79
85
|
@mailer_base_classes = Array(config.fetch("mailer_base_classes")).map(&:to_s)
|
|
80
86
|
@views_root = config.fetch("views_root").to_s
|
|
81
|
-
@mailer_index = nil
|
|
82
|
-
@load_error = nil
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
# File-level: load-error + the missing-view check (anchored on the
|
|
@@ -88,46 +92,22 @@ module Rigor
|
|
|
88
92
|
# arity) runs over the engine-owned walk via the node_rule below
|
|
89
93
|
# (ADR-37). The mailer index is lazily loaded + memoised, shared.
|
|
90
94
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
91
|
-
index =
|
|
92
|
-
return [load_error_diagnostic(path)] if index.nil? &&
|
|
95
|
+
index = producer_value(:mailer_index)
|
|
96
|
+
return [load_error_diagnostic(path)] if index.nil? && producer_error(:mailer_index)
|
|
93
97
|
return [] if index.nil? || index.empty?
|
|
94
98
|
|
|
95
99
|
missing_view_diagnostics(path, index)
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
99
|
-
index =
|
|
103
|
+
index = producer_value(:mailer_index)
|
|
100
104
|
next [] if index.nil? || index.empty?
|
|
101
105
|
|
|
102
|
-
Analyzer.violations_for(call_node: node, mailer_index: index)
|
|
103
|
-
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
104
|
-
end
|
|
106
|
+
diagnostics_for(Analyzer.violations_for(call_node: node, mailer_index: index), path: path, node: node)
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
private
|
|
108
110
|
|
|
109
|
-
def mailer_index_or_nil
|
|
110
|
-
return @mailer_index if @mailer_index
|
|
111
|
-
|
|
112
|
-
# Two-glob descriptor: every mailer class under
|
|
113
|
-
# `mailer_search_paths` AND every view template under
|
|
114
|
-
# `views_root`. Without explicit enumeration the cache
|
|
115
|
-
# invalidates only on files the `IoBoundary` has already
|
|
116
|
-
# read in the current process — empty on the first call
|
|
117
|
-
# of a fresh process, so warm hits would serve stale
|
|
118
|
-
# `MailerIndex` data after mailers are added / removed or
|
|
119
|
-
# view templates are added (`view_exists?` failures aren't
|
|
120
|
-
# recorded, so the auto-built descriptor cannot detect a
|
|
121
|
-
# newly-added view).
|
|
122
|
-
mailer_d = glob_descriptor(@mailer_search_paths, "**/*.rb")
|
|
123
|
-
view_d = glob_descriptor([@views_root], "**/*")
|
|
124
|
-
descriptor = Rigor::Cache::Descriptor.compose(mailer_d, view_d)
|
|
125
|
-
@mailer_index = cache_for(:mailer_index, params: {}, descriptor: descriptor).call
|
|
126
|
-
rescue StandardError => e
|
|
127
|
-
@load_error = "rigor-actionmailer: failed to discover mailers: #{e.class}: #{e.message}"
|
|
128
|
-
nil
|
|
129
|
-
end
|
|
130
|
-
|
|
131
111
|
# Anchors `missing-view` diagnostics on the mailer file
|
|
132
112
|
# itself: when the file currently being analysed is the
|
|
133
113
|
# mailer's source file, emit one diagnostic per missing
|
|
@@ -158,9 +138,10 @@ module Rigor
|
|
|
158
138
|
end
|
|
159
139
|
|
|
160
140
|
def load_error_diagnostic(path)
|
|
141
|
+
error = producer_error(:mailer_index)
|
|
161
142
|
Rigor::Analysis::Diagnostic.new(
|
|
162
143
|
path: path, line: 1, column: 1,
|
|
163
|
-
message:
|
|
144
|
+
message: "rigor-actionmailer: failed to discover mailers: #{error.class}: #{error.message}",
|
|
164
145
|
severity: :warning,
|
|
165
146
|
rule: "load-error"
|
|
166
147
|
)
|
|
@@ -33,9 +33,8 @@ module Rigor
|
|
|
33
33
|
# Phase 2 — filter-chain DSL methods. Each takes a
|
|
34
34
|
# variadic list of filter names (Symbols / Strings) plus
|
|
35
35
|
# optional `only:` / `except:` / `if:` / `unless:`
|
|
36
|
-
# modifiers.
|
|
37
|
-
#
|
|
38
|
-
# is not yet validated (Phase 2.5).
|
|
36
|
+
# modifiers. Only the filter NAMES are validated; the
|
|
37
|
+
# `only:`/`except:` action-name arguments are not (deferred).
|
|
39
38
|
FILTER_DSL_METHODS = %i[
|
|
40
39
|
before_action after_action around_action
|
|
41
40
|
skip_before_action skip_after_action skip_around_action
|
|
@@ -43,18 +42,14 @@ module Rigor
|
|
|
43
42
|
].freeze
|
|
44
43
|
|
|
45
44
|
# Phase 3 — render-target template extensions checked in
|
|
46
|
-
# priority order.
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# `.
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# Configurable extension list is queued — see the
|
|
55
|
-
# `external-author plugin SKILL` track (v0.2.0). For now
|
|
56
|
-
# this set is wide enough to cover the surveyed real-world
|
|
57
|
-
# projects without leaking FPs.
|
|
45
|
+
# priority order. Covers the engines used by surveyed
|
|
46
|
+
# projects: ERB (Rails default — `.html.erb`, `.text.erb`),
|
|
47
|
+
# HAML (Mastodon, Solidus admin — `.html.haml`), Slim, and
|
|
48
|
+
# JSON (`.json.jbuilder` plus `.json.erb` for hand-rolled API
|
|
49
|
+
# responses). When a template exists under any of these
|
|
50
|
+
# extensions, the missing-template diagnostic stays silent.
|
|
51
|
+
# A configurable extension list is deferred; this set is wide
|
|
52
|
+
# enough to cover surveyed real-world projects without FPs.
|
|
58
53
|
RENDER_TEMPLATE_EXTENSIONS = %w[
|
|
59
54
|
.html.erb
|
|
60
55
|
.text.erb
|
|
@@ -167,8 +162,7 @@ module Rigor
|
|
|
167
162
|
# list (looked up via the model_index fact published by
|
|
168
163
|
# `rigor-activerecord`). Calls whose `:require` argument is a
|
|
169
164
|
# non-literal Symbol are passed through; namespaced models
|
|
170
|
-
# (`params.require(:admin_user)` → `Admin::User`) are deferred
|
|
171
|
-
# Phase 1.5 follow-up.
|
|
165
|
+
# (`params.require(:admin_user)` → `Admin::User`) are deferred.
|
|
172
166
|
#
|
|
173
167
|
# @param call_node [Prism::Node]
|
|
174
168
|
# @param model_index [Hash{String => Hash}]
|
|
@@ -68,17 +68,14 @@ module Rigor
|
|
|
68
68
|
class Actionpack < Rigor::Plugin::Base
|
|
69
69
|
manifest(
|
|
70
70
|
id: "actionpack",
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
# Nested-module qualification is preserved — a
|
|
77
|
-
# `module Admin; class DomainBlocksController; end` file still
|
|
71
|
+
# ADR-37: the four phases (helper / filter / render /
|
|
72
|
+
# strong-params) run per-call over the engine-owned walk;
|
|
73
|
+
# the enclosing controller is read from the node-rule
|
|
74
|
+
# `NodeContext` ancestors. Nested-module qualification is
|
|
75
|
+
# preserved — `module Admin; class DomainBlocksController`
|
|
78
76
|
# resolves as `Admin::DomainBlocksController` (matching the
|
|
79
|
-
# `ControllerDiscoverer`), so render paths
|
|
80
|
-
#
|
|
81
|
-
# nested controllers are unchanged.
|
|
77
|
+
# `ControllerDiscoverer`), so render paths and filter-chain
|
|
78
|
+
# validation on nested controllers are correct.
|
|
82
79
|
version: "0.8.0",
|
|
83
80
|
description: "Validates Action Pack route-helper calls and filter chains inside controllers.",
|
|
84
81
|
config_schema: {
|
|
@@ -91,27 +88,22 @@ module Rigor
|
|
|
91
88
|
]
|
|
92
89
|
)
|
|
93
90
|
|
|
94
|
-
# Phase 2 cached producer — the controller index built
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
91
|
+
# Phase 2 cached producer — the controller index built from
|
|
92
|
+
# `controller_search_paths`. `watch:` (ADR-60 WD3) covers every
|
|
93
|
+
# `.rb` file under those roots so the cache invalidates when a
|
|
94
|
+
# controller is added, removed, or edited; the discoverer's
|
|
95
|
+
# in-block `io_boundary` reads are captured into the dependency
|
|
96
|
+
# descriptor too, so no explicit priming is needed.
|
|
97
|
+
producer :controller_index, watch: -> { [[@controller_search_paths, "**/*.rb"]] } do |_params|
|
|
100
98
|
ControllerDiscoverer.new(
|
|
101
99
|
io_boundary: io_boundary,
|
|
102
100
|
search_paths: @controller_search_paths
|
|
103
101
|
).discover
|
|
104
102
|
end
|
|
105
103
|
|
|
106
|
-
def init(
|
|
107
|
-
@services = services
|
|
104
|
+
def init(_services)
|
|
108
105
|
@controller_search_paths = Array(config.fetch("controller_search_paths")).map(&:to_s)
|
|
109
106
|
@view_search_paths = Array(config.fetch("view_search_paths")).map(&:to_s)
|
|
110
|
-
@helper_table = nil
|
|
111
|
-
@helper_table_resolved = false
|
|
112
|
-
@controller_index = nil
|
|
113
|
-
@model_index_value = nil
|
|
114
|
-
@model_index_resolved = false
|
|
115
107
|
end
|
|
116
108
|
|
|
117
109
|
# ADR-37 — the four Action Pack phases run per-call over the
|
|
@@ -123,16 +115,16 @@ module Rigor
|
|
|
123
115
|
# The filter / render phases read the enclosing controller from the
|
|
124
116
|
# node-rule `NodeContext` ancestors (its fifth block argument).
|
|
125
117
|
|
|
126
|
-
# Phase 4 — route-helper consumption.
|
|
118
|
+
# Phase 4 — route-helper consumption. `:helper_table` is
|
|
119
|
+
# rigor-rails-routes's published fact (ADR-9), read lazily via
|
|
120
|
+
# `read_fact`.
|
|
127
121
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
128
122
|
next [] unless controller_file?(path)
|
|
129
123
|
|
|
130
|
-
table = helper_table
|
|
124
|
+
table = read_fact(plugin_id: "rails-routes", name: :helper_table)
|
|
131
125
|
next [] if table.nil? || table.empty?
|
|
132
126
|
|
|
133
|
-
Analyzer.helper_violations_for(call_node: node, helper_table: table)
|
|
134
|
-
diagnostic(node, path: path, location: v.location, message: v.message, severity: v.severity, rule: v.rule)
|
|
135
|
-
end
|
|
127
|
+
diagnostics_for(Analyzer.helper_violations_for(call_node: node, helper_table: table), path: path, node: node)
|
|
136
128
|
end
|
|
137
129
|
|
|
138
130
|
# Phase 2 — filter-chain validation. Skips silently when the
|
|
@@ -141,12 +133,13 @@ module Rigor
|
|
|
141
133
|
node_rule Prism::CallNode do |node, _scope, path, _fc, context|
|
|
142
134
|
next [] unless controller_file?(path)
|
|
143
135
|
|
|
144
|
-
index =
|
|
136
|
+
index = producer_value(:controller_index)
|
|
145
137
|
next [] if index.nil? || index.empty?
|
|
146
138
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
diagnostics_for(
|
|
140
|
+
Analyzer.filter_violations_for(call_node: node, ancestors: context.ancestors, controller_index: index),
|
|
141
|
+
path: path, node: node
|
|
142
|
+
)
|
|
150
143
|
end
|
|
151
144
|
|
|
152
145
|
# Phase 3 — render-target validation against the configured
|
|
@@ -157,12 +150,13 @@ module Rigor
|
|
|
157
150
|
node_rule Prism::CallNode do |node, _scope, path, _fc, context|
|
|
158
151
|
next [] unless controller_file?(path)
|
|
159
152
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
153
|
+
diagnostics_for(
|
|
154
|
+
Analyzer.render_violations_for(
|
|
155
|
+
call_node: node, ancestors: context.ancestors, path: path,
|
|
156
|
+
view_search_roots: @view_search_paths, controller_index: producer_value(:controller_index)
|
|
157
|
+
),
|
|
158
|
+
path: path, node: node
|
|
159
|
+
)
|
|
166
160
|
end
|
|
167
161
|
|
|
168
162
|
# Phase 1 — strong-parameter validation. Reads the `:model_index`
|
|
@@ -173,74 +167,14 @@ module Rigor
|
|
|
173
167
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
174
168
|
next [] unless controller_file?(path)
|
|
175
169
|
|
|
176
|
-
index = model_index
|
|
170
|
+
index = read_fact(plugin_id: "activerecord", name: :model_index)
|
|
177
171
|
next [] if index.nil? || index.empty?
|
|
178
172
|
|
|
179
|
-
Analyzer.permit_violations_for(call_node: node, model_index: index)
|
|
180
|
-
diagnostic(node, path: path, location: v.location, message: v.message, severity: v.severity, rule: v.rule)
|
|
181
|
-
end
|
|
173
|
+
diagnostics_for(Analyzer.permit_violations_for(call_node: node, model_index: index), path: path, node: node)
|
|
182
174
|
end
|
|
183
175
|
|
|
184
176
|
private
|
|
185
177
|
|
|
186
|
-
def controller_index_or_nil
|
|
187
|
-
return @controller_index if @controller_index
|
|
188
|
-
|
|
189
|
-
# Read project source first so the IoBoundary's
|
|
190
|
-
# FileEntry digests get captured into the descriptor
|
|
191
|
-
# before `cache_for` snapshots it (mirrors
|
|
192
|
-
# rigor-rails-routes / rigor-pundit's pattern).
|
|
193
|
-
prime_io_boundary_for_index
|
|
194
|
-
@controller_index = cache_for(:controller_index, params: {}).call
|
|
195
|
-
rescue StandardError
|
|
196
|
-
nil
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def prime_io_boundary_for_index
|
|
200
|
-
@controller_search_paths.each do |root|
|
|
201
|
-
absolute = File.expand_path(root)
|
|
202
|
-
next unless File.directory?(absolute)
|
|
203
|
-
|
|
204
|
-
Dir.glob(File.join(absolute, "**", "*.rb")).each do |path|
|
|
205
|
-
io_boundary.read_file(path)
|
|
206
|
-
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
207
|
-
nil
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Lazily resolves the helper table from the cross-plugin
|
|
213
|
-
# fact store. The cache is per-run because the runner
|
|
214
|
-
# builds a fresh `FactStore` per invocation; memoizing on
|
|
215
|
-
# the plugin instance saves the per-file `read` while
|
|
216
|
-
# still picking up a freshly-published table on the next
|
|
217
|
-
# `bundle exec rigor check` run.
|
|
218
|
-
def helper_table
|
|
219
|
-
return @helper_table if @helper_table_resolved
|
|
220
|
-
|
|
221
|
-
@helper_table = @services.fact_store.read(
|
|
222
|
-
plugin_id: "rails-routes", name: :helper_table
|
|
223
|
-
)
|
|
224
|
-
@helper_table_resolved = true
|
|
225
|
-
@helper_table
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Phase 1 — lazily reads the cross-plugin :model_index
|
|
229
|
-
# fact from rigor-activerecord. The cache is per-run
|
|
230
|
-
# because the runner builds a fresh FactStore per
|
|
231
|
-
# invocation; memoizing on the plugin instance saves the
|
|
232
|
-
# per-file read while still picking up a freshly
|
|
233
|
-
# published index on the next `bundle exec rigor check`.
|
|
234
|
-
def model_index
|
|
235
|
-
return @model_index_value if @model_index_resolved
|
|
236
|
-
|
|
237
|
-
@model_index_value = @services.fact_store.read(
|
|
238
|
-
plugin_id: "activerecord", name: :model_index
|
|
239
|
-
)
|
|
240
|
-
@model_index_resolved = true
|
|
241
|
-
@model_index_value
|
|
242
|
-
end
|
|
243
|
-
|
|
244
178
|
def controller_file?(path)
|
|
245
179
|
@controller_search_paths.any? do |root|
|
|
246
180
|
# The runner may pass `path` as either an absolute
|
|
@@ -12,8 +12,9 @@ module Rigor
|
|
|
12
12
|
# (`Float::INFINITY` for the upper bound when `*args`
|
|
13
13
|
# is present). `keyword_required` lists any required
|
|
14
14
|
# keyword arguments — Active Job supports keyword args
|
|
15
|
-
# but they're rare in user code, so the analyzer
|
|
16
|
-
# validates positional arity
|
|
15
|
+
# but they're rare in user code, so the analyzer
|
|
16
|
+
# validates positional arity only (keyword arity
|
|
17
|
+
# validation is deferred).
|
|
17
18
|
class JobIndex
|
|
18
19
|
Entry = Data.define(:class_name, :min_arity, :max_arity, :keyword_required) do
|
|
19
20
|
# Flexible-friendly textual form of the arity for
|
|
@@ -48,12 +48,12 @@ module Rigor
|
|
|
48
48
|
}
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
-
# Cached: discovered job index.
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# `
|
|
55
|
-
#
|
|
56
|
-
producer :job_index do |_params|
|
|
51
|
+
# Cached: discovered job index. `watch:` (ADR-60 WD3) covers
|
|
52
|
+
# every `.rb` under `job_search_paths` so the cache invalidates
|
|
53
|
+
# when a job is added, removed, or edited; the discoverer's
|
|
54
|
+
# in-block `IoBoundary` reads are captured into the record-and-
|
|
55
|
+
# validate dependency descriptor after the block runs.
|
|
56
|
+
producer :job_index, watch: -> { [[@job_search_paths, "**/*.rb"]] } do |_params|
|
|
57
57
|
JobDiscoverer.new(
|
|
58
58
|
io_boundary: io_boundary,
|
|
59
59
|
search_paths: @job_search_paths,
|
|
@@ -64,50 +64,33 @@ module Rigor
|
|
|
64
64
|
def init(_services)
|
|
65
65
|
@job_search_paths = Array(config.fetch("job_search_paths")).map(&:to_s)
|
|
66
66
|
@job_base_classes = Array(config.fetch("job_base_classes")).map(&:to_s)
|
|
67
|
-
@job_index = nil
|
|
68
|
-
@load_error = nil
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
# File-level only: the load-error emission. Per-call arity
|
|
72
70
|
# validation runs over the engine-owned walk via the node_rule
|
|
73
71
|
# below (ADR-37). The job index is lazily loaded + memoised by
|
|
74
|
-
#
|
|
72
|
+
# `producer_value`, shared by both surfaces.
|
|
75
73
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
76
|
-
index =
|
|
77
|
-
return [load_error_diagnostic(path)] if index.nil? &&
|
|
74
|
+
index = producer_value(:job_index)
|
|
75
|
+
return [load_error_diagnostic(path)] if index.nil? && producer_error(:job_index)
|
|
78
76
|
|
|
79
77
|
[]
|
|
80
78
|
end
|
|
81
79
|
|
|
82
80
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
83
|
-
index =
|
|
81
|
+
index = producer_value(:job_index)
|
|
84
82
|
next [] if index.nil? || index.empty?
|
|
85
83
|
|
|
86
|
-
Analyzer.violations_for(call_node: node, job_index: index)
|
|
87
|
-
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
88
|
-
end
|
|
84
|
+
diagnostics_for(Analyzer.violations_for(call_node: node, job_index: index), path: path, node: node)
|
|
89
85
|
end
|
|
90
86
|
|
|
91
87
|
private
|
|
92
88
|
|
|
93
|
-
def job_index_or_nil
|
|
94
|
-
return @job_index if @job_index
|
|
95
|
-
|
|
96
|
-
# Read-then-cache pattern: the discoverer's
|
|
97
|
-
# IoBoundary reads happen INSIDE `discover`, which is
|
|
98
|
-
# invoked through `cache_for`'s producer block. The
|
|
99
|
-
# boundary's accumulated FileEntry digests get
|
|
100
|
-
# captured into the descriptor at cache_for time.
|
|
101
|
-
@job_index = cache_for(:job_index, params: {}).call
|
|
102
|
-
rescue StandardError => e
|
|
103
|
-
@load_error = "rigor-activejob: failed to discover jobs: #{e.class}: #{e.message}"
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
106
|
-
|
|
107
89
|
def load_error_diagnostic(path)
|
|
90
|
+
error = producer_error(:job_index)
|
|
108
91
|
Rigor::Analysis::Diagnostic.new(
|
|
109
92
|
path: path, line: 1, column: 1,
|
|
110
|
-
message:
|
|
93
|
+
message: "rigor-activejob: failed to discover jobs: #{error.class}: #{error.message}",
|
|
111
94
|
severity: :warning,
|
|
112
95
|
rule: "load-error"
|
|
113
96
|
)
|
|
@@ -227,8 +227,8 @@ module Rigor
|
|
|
227
227
|
# Recognised single-instance and collection association
|
|
228
228
|
# DSL methods. The kind drives the eventual return-type
|
|
229
229
|
# contribution: singular associations narrow to
|
|
230
|
-
# `Nominal[Target] | nil`, plural ones
|
|
231
|
-
#
|
|
230
|
+
# `Nominal[Target] | nil`, plural ones narrow to
|
|
231
|
+
# `ActiveRecord::Relation[Target]`.
|
|
232
232
|
#
|
|
233
233
|
# `composed_of` value-object aggregations and
|
|
234
234
|
# `delegated_type` roles are folded in here too — both
|
|
@@ -447,8 +447,8 @@ module Rigor
|
|
|
447
447
|
|
|
448
448
|
# `scope :active, -> { ... }`. Records the scope name
|
|
449
449
|
# only (the body is intentionally NOT introspected —
|
|
450
|
-
#
|
|
451
|
-
#
|
|
450
|
+
# the caller contributes `ActiveRecord::Relation[Model]`
|
|
451
|
+
# based on the name alone via `class_scope_return_type`).
|
|
452
452
|
def lookup_scopes(body)
|
|
453
453
|
return [] if body.nil?
|
|
454
454
|
|