rigortype 0.1.19 → 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/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- 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/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -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 +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -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 +2 -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 +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- 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 +49 -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/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 +20 -28
- 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/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- 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/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- 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 +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- 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 +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- 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 +14 -2
- data/lib/rigor/scope.rb +54 -11
- 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/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -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 +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- 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 +7 -9
- 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 +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- 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 +1 -1
- 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 +5 -5
- 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 +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- 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 +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../trinary"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Type
|
|
7
|
+
# Supplies the lattice-membership trio for the "plain" carriers — the
|
|
8
|
+
# concrete value types that are neither a lattice extreme (`Top` /
|
|
9
|
+
# `Bot` / `Dynamic`) nor a wrapper that computes membership from an
|
|
10
|
+
# inner type.
|
|
11
|
+
#
|
|
12
|
+
# Every such carrier answers `top` / `bot` / `dynamic` with the same
|
|
13
|
+
# `Trinary.no` ("this value is not that lattice point"), so the trio
|
|
14
|
+
# lived as a byte-identical copy in a dozen carriers. The extremes
|
|
15
|
+
# override the relevant member (`Top#top` / `Bot#bot` /
|
|
16
|
+
# `Dynamic#dynamic` answer `Trinary.yes`) and the delegators (`App`,
|
|
17
|
+
# `Difference`, `Refined`, `Union`) compute `dynamic` from their inner
|
|
18
|
+
# type(s); none of those include this module.
|
|
19
|
+
#
|
|
20
|
+
# Mirrors the existing {AcceptanceRouter} / `ValueSemantics` mixins —
|
|
21
|
+
# narrow trait sharing, never carrier inheritance (which the type-object
|
|
22
|
+
# contract forbids).
|
|
23
|
+
module PlainLattice
|
|
24
|
+
def top
|
|
25
|
+
Trinary.no
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def bot
|
|
29
|
+
Trinary.no
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def dynamic
|
|
33
|
+
Trinary.no
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/rigor/type/refined.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
3
5
|
require_relative "../trinary"
|
|
4
6
|
require_relative "../value_semantics"
|
|
5
7
|
require_relative "acceptance_router"
|
|
@@ -104,18 +106,31 @@ module Rigor
|
|
|
104
106
|
# so callers can pass any `Constant#value` without a
|
|
105
107
|
# type-prefilter.
|
|
106
108
|
#
|
|
107
|
-
# Plugin-contributed predicates
|
|
108
|
-
#
|
|
109
|
-
# built-in catalogue.
|
|
109
|
+
# Plugin-contributed predicates are not yet wired; today
|
|
110
|
+
# the table covers the built-in catalogue.
|
|
110
111
|
#
|
|
111
112
|
# Recogniser policy:
|
|
112
113
|
#
|
|
113
|
-
# - `:numeric`
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
114
|
+
# - `:numeric` recognises a string that is a *single Ruby
|
|
115
|
+
# numeric literal* — exactly the syntax that, written in
|
|
116
|
+
# Ruby source, evaluates to an `Integer` / `Float` /
|
|
117
|
+
# `Rational` / `Complex`. The recogniser delegates to the
|
|
118
|
+
# real Ruby parser ({Refined.ruby_numeric_literal?} via
|
|
119
|
+
# Prism), so it tracks Ruby's grammar precisely: decimal /
|
|
120
|
+
# `0x` hex / `0o` (or leading-zero) octal / `0b` binary /
|
|
121
|
+
# `0d` decimal integers, underscore digit separators
|
|
122
|
+
# (`1_000`), decimal fractions and scientific floats
|
|
123
|
+
# (`1.5`, `1E-5`), and the `r` rational / `i` imaginary
|
|
124
|
+
# suffixes (`1r`, `2i`, `0xffr`). A single leading sign is
|
|
125
|
+
# folded into the literal (`-1`, `+1.5`), but a doubled
|
|
126
|
+
# sign (`--1`, `++1`) parses as a unary-operator chain — a
|
|
127
|
+
# `CallNode`, not a literal — and is rejected, as are
|
|
128
|
+
# multi-dot junk (`1.2.3`), partial literals (`0x`, `1_`),
|
|
129
|
+
# whitespace-padded strings, and — crucially — non-ASCII
|
|
130
|
+
# "digits" (full-width `1`, superscript `²`, other Unicode
|
|
131
|
+
# number characters): Ruby's lexer only accepts `[0-9]` in
|
|
132
|
+
# a numeric literal, so those are `CallNode`s too. The
|
|
133
|
+
# stricter base-N predicates below remain proper subsets.
|
|
119
134
|
# - `:decimal_int` is "what `Integer(s, 10)` would parse
|
|
120
135
|
# without remainder" — one or more decimal digits,
|
|
121
136
|
# optional leading sign, no whitespace, no fractional
|
|
@@ -127,20 +142,64 @@ module Rigor
|
|
|
127
142
|
# not octal-int-string. This matches the typical user
|
|
128
143
|
# intent — a refinement marks a string that "looks like
|
|
129
144
|
# octal", not "happens to be base-8 valid".
|
|
130
|
-
NUMERIC_STRING_PATTERN = /\A-?\d+(?:\.\d+)?\z/
|
|
131
145
|
DECIMAL_INT_STRING_PATTERN = /\A-?\d+\z/
|
|
132
146
|
OCTAL_INT_STRING_PATTERN = /\A-?(?:0[oO][0-7]+|0[0-7]+)\z/
|
|
133
147
|
HEX_INT_STRING_PATTERN = /\A-?0[xX][0-9a-fA-F]+\z/
|
|
134
|
-
private_constant :
|
|
148
|
+
private_constant :DECIMAL_INT_STRING_PATTERN,
|
|
135
149
|
:OCTAL_INT_STRING_PATTERN, :HEX_INT_STRING_PATTERN
|
|
136
150
|
|
|
151
|
+
# Prism node classes that represent a numeric literal. A
|
|
152
|
+
# string is a numeric-string exactly when the parser reduces
|
|
153
|
+
# the whole input to a single one of these (the leading sign
|
|
154
|
+
# is already folded into the literal by the parser).
|
|
155
|
+
NUMERIC_LITERAL_NODES = [
|
|
156
|
+
Prism::IntegerNode,
|
|
157
|
+
Prism::FloatNode,
|
|
158
|
+
Prism::RationalNode,
|
|
159
|
+
Prism::ImaginaryNode
|
|
160
|
+
].freeze
|
|
161
|
+
private_constant :NUMERIC_LITERAL_NODES
|
|
162
|
+
|
|
163
|
+
# Cheap pre-filter applied before invoking the parser: every
|
|
164
|
+
# Ruby numeric literal starts with an ASCII digit, optionally
|
|
165
|
+
# preceded by exactly one sign. Strings that fail this never
|
|
166
|
+
# reach Prism (the common non-numeric case stays allocation-
|
|
167
|
+
# and parse-free).
|
|
168
|
+
NUMERIC_LITERAL_PREFIX = /\A[+-]?\d/
|
|
169
|
+
private_constant :NUMERIC_LITERAL_PREFIX
|
|
170
|
+
|
|
171
|
+
# @param value [Object] typically a `Constant#value`
|
|
172
|
+
# @return [Boolean] true when `value` is a String that is a
|
|
173
|
+
# single, complete Ruby numeric literal. Total over
|
|
174
|
+
# arbitrary input — never raises (Prism reports malformed
|
|
175
|
+
# input through `errors`, it does not throw).
|
|
176
|
+
def self.ruby_numeric_literal?(value)
|
|
177
|
+
return false unless value.is_a?(String)
|
|
178
|
+
return false if value.empty?
|
|
179
|
+
# A numeric literal carries no whitespace; reject any
|
|
180
|
+
# leading / trailing / interior space so the *whole* string
|
|
181
|
+
# must be the literal (Prism would otherwise accept a
|
|
182
|
+
# trailing-space `"1 "`).
|
|
183
|
+
return false if value.match?(/\s/)
|
|
184
|
+
return false unless value.match?(NUMERIC_LITERAL_PREFIX)
|
|
185
|
+
|
|
186
|
+
result = Prism.parse(value)
|
|
187
|
+
return false unless result.errors.empty?
|
|
188
|
+
|
|
189
|
+
body = result.value.statements&.body
|
|
190
|
+
return false unless body && body.size == 1
|
|
191
|
+
|
|
192
|
+
node = body.first
|
|
193
|
+
NUMERIC_LITERAL_NODES.any? { |klass| node.is_a?(klass) }
|
|
194
|
+
end
|
|
195
|
+
|
|
137
196
|
PREDICATES = {
|
|
138
197
|
lowercase: ->(v) { v.is_a?(String) && v == v.downcase },
|
|
139
198
|
not_lowercase: ->(v) { v.is_a?(String) && v != v.downcase },
|
|
140
199
|
uppercase: ->(v) { v.is_a?(String) && v == v.upcase },
|
|
141
200
|
not_uppercase: ->(v) { v.is_a?(String) && v != v.upcase },
|
|
142
|
-
numeric: ->(v) {
|
|
143
|
-
not_numeric: ->(v) { v.is_a?(String) && !
|
|
201
|
+
numeric: ->(v) { ruby_numeric_literal?(v) },
|
|
202
|
+
not_numeric: ->(v) { v.is_a?(String) && !ruby_numeric_literal?(v) },
|
|
144
203
|
decimal_int: ->(v) { v.is_a?(String) && DECIMAL_INT_STRING_PATTERN.match?(v) },
|
|
145
204
|
octal_int: ->(v) { v.is_a?(String) && OCTAL_INT_STRING_PATTERN.match?(v) },
|
|
146
205
|
hex_int: ->(v) { v.is_a?(String) && HEX_INT_STRING_PATTERN.match?(v) },
|
data/lib/rigor/type/singleton.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../trinary"
|
|
4
4
|
require_relative "../value_semantics"
|
|
5
5
|
require_relative "acceptance_router"
|
|
6
|
+
require_relative "plain_lattice"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Type
|
|
@@ -34,17 +35,7 @@ module Rigor
|
|
|
34
35
|
"singleton(#{class_name})"
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
Trinary.no
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def bot
|
|
42
|
-
Trinary.no
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def dynamic
|
|
46
|
-
Trinary.no
|
|
47
|
-
end
|
|
38
|
+
include Rigor::Type::PlainLattice
|
|
48
39
|
|
|
49
40
|
include Rigor::Type::AcceptanceRouter
|
|
50
41
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../trinary"
|
|
4
|
+
require_relative "../value_semantics"
|
|
5
|
+
require_relative "acceptance_router"
|
|
6
|
+
require_relative "plain_lattice"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Type
|
|
10
|
+
# The class object produced by `Struct.new(:x, :y)` (ADR-48 Struct
|
|
11
|
+
# follow-up). The mutable sibling of {DataClass}: it models the *class*
|
|
12
|
+
# (the value bound to `Point` in `Point = Struct.new(:x, :y)`, or the
|
|
13
|
+
# anonymous superclass in `class Point < Struct.new(:x, :y)`), carrying
|
|
14
|
+
# the ordered member-name list so `Point.new(...)` can materialise a
|
|
15
|
+
# {StructInstance}.
|
|
16
|
+
#
|
|
17
|
+
# `keyword_init` records the `Struct.new(..., keyword_init: true)` flag
|
|
18
|
+
# so `.new` only materialises a precise instance for the matching call
|
|
19
|
+
# form — a positional `.new(1, 2)` on a `keyword_init: true` struct, or
|
|
20
|
+
# a keyword `.new(x: 1)` on a positional struct, is a different runtime
|
|
21
|
+
# shape and must degrade rather than fold a wrong member map.
|
|
22
|
+
#
|
|
23
|
+
# `class_name` carries the binding name when known (the named-subclass
|
|
24
|
+
# form) and is `nil` for the anonymous result of a bare `Struct.new(...)`
|
|
25
|
+
# before it is assigned to a constant.
|
|
26
|
+
#
|
|
27
|
+
# Equality and hashing are structural over the member list, the class
|
|
28
|
+
# name, and the keyword-init flag.
|
|
29
|
+
#
|
|
30
|
+
# See docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
|
|
31
|
+
class StructClass
|
|
32
|
+
attr_reader :members, :class_name, :keyword_init
|
|
33
|
+
|
|
34
|
+
# @param members [Array<Symbol>] ordered member names.
|
|
35
|
+
# @param class_name [String, nil] the bound class name, or nil for
|
|
36
|
+
# the anonymous `Struct.new(...)` result.
|
|
37
|
+
# @param keyword_init [Boolean] the `keyword_init:` flag.
|
|
38
|
+
def initialize(members, class_name = nil, keyword_init: false)
|
|
39
|
+
unless members.is_a?(Array) && members.all?(Symbol)
|
|
40
|
+
raise ArgumentError, "members must be an Array of Symbols, got #{members.inspect}"
|
|
41
|
+
end
|
|
42
|
+
unless class_name.nil? || (class_name.is_a?(String) && !class_name.empty?)
|
|
43
|
+
raise ArgumentError, "class_name must be a non-empty String or nil, got #{class_name.inspect}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@members = members.dup.freeze
|
|
47
|
+
@class_name = class_name&.freeze
|
|
48
|
+
@keyword_init = keyword_init ? true : false
|
|
49
|
+
freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def describe(_verbosity = :short)
|
|
53
|
+
return "singleton(#{class_name})" if class_name
|
|
54
|
+
|
|
55
|
+
"Struct.new(#{members.map(&:inspect).join(', ')})"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def erase_to_rbs
|
|
59
|
+
"singleton(#{class_name || 'Struct'})"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
include Rigor::Type::PlainLattice
|
|
63
|
+
|
|
64
|
+
include Rigor::Type::AcceptanceRouter
|
|
65
|
+
|
|
66
|
+
include Rigor::ValueSemantics
|
|
67
|
+
|
|
68
|
+
value_fields :members, :class_name, :keyword_init
|
|
69
|
+
|
|
70
|
+
def inspect
|
|
71
|
+
"#<Rigor::Type::StructClass #{describe(:short)}>"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../trinary"
|
|
4
|
+
require_relative "../value_semantics"
|
|
5
|
+
require_relative "acceptance_router"
|
|
6
|
+
require_relative "plain_lattice"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Type
|
|
10
|
+
# A `Struct.new` value instance (ADR-48 Struct follow-up) —
|
|
11
|
+
# `Point.new(1, 2)`. The mutable sibling of {DataInstance}: a closed,
|
|
12
|
+
# total, class-tagged member map (member name -> value type),
|
|
13
|
+
# HashShape-shaped but nominal.
|
|
14
|
+
#
|
|
15
|
+
# Unlike {DataInstance}, a `Struct` instance is **mutable** — `s.x = v`,
|
|
16
|
+
# `s[:x] = v`, and escape can invalidate the member map. The folding
|
|
17
|
+
# tier therefore only projects member reads off a **fresh** instance
|
|
18
|
+
# (the transient receiver of a `.new(...).x` / `.with(...).x` chain,
|
|
19
|
+
# which provably cannot have been mutated between materialisation and
|
|
20
|
+
# the read); a read off a *stored* binding degrades to `Dynamic[top]`
|
|
21
|
+
# rather than fold a possibly-stale member value. Promoting the
|
|
22
|
+
# fold to mutation-free bound locals is the deferred slice 3 (see ADR).
|
|
23
|
+
#
|
|
24
|
+
# That mutability-gating lives in the dispatch tier (`StructFolding`),
|
|
25
|
+
# not the carrier: the carrier itself just records the member map. Like
|
|
26
|
+
# {DataInstance}, non-folded methods project to the `Struct` nominal (or
|
|
27
|
+
# the tagged class) through {RbsDispatch}'s `receiver_descriptor`, so
|
|
28
|
+
# non-member calls resolve without mis-firing undefined-method.
|
|
29
|
+
#
|
|
30
|
+
# Equality and hashing are structural over the (member -> type) map and
|
|
31
|
+
# the class name.
|
|
32
|
+
#
|
|
33
|
+
# See docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
|
|
34
|
+
class StructInstance
|
|
35
|
+
attr_reader :members, :class_name
|
|
36
|
+
|
|
37
|
+
# @param members [Hash{Symbol => Rigor::Type}] ordered member -> type
|
|
38
|
+
# map. Every declared member is present (Struct instances are total).
|
|
39
|
+
# @param class_name [String, nil] the tagging class name, or nil for
|
|
40
|
+
# an instance of an anonymous `Struct.new(...)` class.
|
|
41
|
+
def initialize(members, class_name = nil)
|
|
42
|
+
unless members.is_a?(Hash) && members.each_key.all?(Symbol)
|
|
43
|
+
raise ArgumentError, "members must be a Hash with Symbol keys, got #{members.inspect}"
|
|
44
|
+
end
|
|
45
|
+
unless class_name.nil? || (class_name.is_a?(String) && !class_name.empty?)
|
|
46
|
+
raise ArgumentError, "class_name must be a non-empty String or nil, got #{class_name.inspect}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@members = members.dup.freeze
|
|
50
|
+
@class_name = class_name&.freeze
|
|
51
|
+
freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Array<Symbol>] ordered member names.
|
|
55
|
+
def member_names
|
|
56
|
+
members.keys
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Rigor::Type, nil] the member's value type, or nil when the
|
|
60
|
+
# name is not a declared member.
|
|
61
|
+
def member_type(name)
|
|
62
|
+
members[name]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def describe(verbosity = :short)
|
|
66
|
+
rendered = members.map { |name, type| "#{name}: #{type.describe(verbosity)}" }
|
|
67
|
+
"#{class_name || 'Struct'}(#{rendered.join(', ')})"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Erases to the tagging class nominal (conservative: the structural
|
|
71
|
+
# members are not RBS-expressible as a class instance). The
|
|
72
|
+
# anonymous case erases to the `Struct` supertype.
|
|
73
|
+
def erase_to_rbs
|
|
74
|
+
name = class_name
|
|
75
|
+
return "Struct" if name.nil?
|
|
76
|
+
|
|
77
|
+
name
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
include Rigor::Type::PlainLattice
|
|
81
|
+
|
|
82
|
+
include Rigor::Type::AcceptanceRouter
|
|
83
|
+
|
|
84
|
+
include Rigor::ValueSemantics
|
|
85
|
+
|
|
86
|
+
value_fields :members, :class_name
|
|
87
|
+
|
|
88
|
+
def inspect
|
|
89
|
+
"#<Rigor::Type::StructInstance #{describe(:short)}>"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/rigor/type/tuple.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../trinary"
|
|
4
4
|
require_relative "../value_semantics"
|
|
5
5
|
require_relative "acceptance_router"
|
|
6
|
+
require_relative "plain_lattice"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Type
|
|
@@ -17,10 +18,9 @@ module Rigor
|
|
|
17
18
|
#
|
|
18
19
|
# Slice 5 phase 1 introduces the carrier and surfaces it from the
|
|
19
20
|
# `ArrayNode` literal handler when every element is a non-splat
|
|
20
|
-
# value. Tuple-aware refinements
|
|
21
|
-
# destructuring
|
|
22
|
-
#
|
|
23
|
-
# {Rigor::Inference::MethodDispatcher::RbsDispatch}.
|
|
21
|
+
# value. Tuple-aware refinements (`tuple[0]`, `tuple.first`,
|
|
22
|
+
# destructuring) are implemented in `ShapeDispatch`, which runs
|
|
23
|
+
# above {Rigor::Inference::MethodDispatcher::RbsDispatch}.
|
|
24
24
|
#
|
|
25
25
|
# Equality and hashing are structural across an ordered, frozen
|
|
26
26
|
# element list. The empty Tuple `Tuple[]` is permitted; the array
|
|
@@ -53,17 +53,7 @@ module Rigor
|
|
|
53
53
|
"[#{elements.map(&:erase_to_rbs).join(', ')}]"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
Trinary.no
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def bot
|
|
61
|
-
Trinary.no
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def dynamic
|
|
65
|
-
Trinary.no
|
|
66
|
-
end
|
|
56
|
+
include Rigor::Type::PlainLattice
|
|
67
57
|
|
|
68
58
|
include Rigor::Type::AcceptanceRouter
|
|
69
59
|
|
data/lib/rigor/type.rb
CHANGED
|
@@ -21,6 +21,8 @@ require_relative "type/tuple"
|
|
|
21
21
|
require_relative "type/hash_shape"
|
|
22
22
|
require_relative "type/data_class"
|
|
23
23
|
require_relative "type/data_instance"
|
|
24
|
+
require_relative "type/struct_class"
|
|
25
|
+
require_relative "type/struct_instance"
|
|
24
26
|
require_relative "type/union"
|
|
25
27
|
require_relative "type/difference"
|
|
26
28
|
require_relative "type/refined"
|
data/lib/rigor/version.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Rigor
|
|
|
26
26
|
# `stream_for` call) so the analyzer knows it can't
|
|
27
27
|
# be sure of every stream name.
|
|
28
28
|
#
|
|
29
|
-
#
|
|
29
|
+
# Intentional limitations:
|
|
30
30
|
#
|
|
31
31
|
# - Direct-superclass match only.
|
|
32
32
|
# - Public-vs-private is not tracked; the framework
|
|
@@ -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.
|
|
@@ -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]
|
|
@@ -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: {
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -75,10 +75,9 @@ module Rigor
|
|
|
75
75
|
"model_base_classes" => { kind: :array, default: %w[ApplicationRecord ActiveRecord::Base] }
|
|
76
76
|
},
|
|
77
77
|
produces: [:model_index],
|
|
78
|
-
# ADR-25 — the bundled `ActiveRecord::Relation` RBS
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
# against.
|
|
78
|
+
# ADR-25 — the bundled `ActiveRecord::Relation` RBS that
|
|
79
|
+
# relation-typed call sites (`has_many` accessors,
|
|
80
|
+
# `Model.where`, scopes) dispatch against.
|
|
82
81
|
signature_paths: ["sig"],
|
|
83
82
|
# ADR-26 — `ActiveRecord::Relation` is an "open" receiver:
|
|
84
83
|
# it delegates an unbounded set of user-defined scopes /
|
|
@@ -89,7 +88,7 @@ module Rigor
|
|
|
89
88
|
)
|
|
90
89
|
|
|
91
90
|
# The class the bundled `sig/active_record/relation.rbs`
|
|
92
|
-
# describes; `
|
|
91
|
+
# describes; `dynamic_return` contributes
|
|
93
92
|
# `ActiveRecord::Relation[Model]` for relation-returning
|
|
94
93
|
# call sites (`has_many` accessors, `Model.where`, scopes).
|
|
95
94
|
RELATION_CLASS_NAME = "ActiveRecord::Relation"
|
|
@@ -261,9 +260,8 @@ module Rigor
|
|
|
261
260
|
names
|
|
262
261
|
end
|
|
263
262
|
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
# `dynamic_return` contract expects.
|
|
263
|
+
# Resolution body for `dynamic_return` — same four-path
|
|
264
|
+
# order, returning the bare type the contract expects.
|
|
267
265
|
def contribution_return_type(call_node, scope)
|
|
268
266
|
return nil unless call_node.is_a?(Prism::CallNode)
|
|
269
267
|
|
|
@@ -11,13 +11,11 @@ module Rigor
|
|
|
11
11
|
# plugin recognised so users can verify the model →
|
|
12
12
|
# attachment mapping the plugin sees.
|
|
13
13
|
#
|
|
14
|
-
# No `:error` diagnostics
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# rely on. A future slice can add `unknown-attachment`
|
|
20
|
-
# similar to `rigor-activerecord`'s `unknown-column`.
|
|
14
|
+
# No `:error` diagnostics here — the `dynamic_return`
|
|
15
|
+
# return-type narrowing carries the type-checking value.
|
|
16
|
+
# An `unknown-attachment` rule (similar to
|
|
17
|
+
# `rigor-activerecord`'s `unknown-column`) is deferred:
|
|
18
|
+
# it requires a coupled receiver-class narrowing pass.
|
|
21
19
|
class Analyzer
|
|
22
20
|
attr_reader :diagnostics
|
|
23
21
|
|