rigortype 0.1.1 → 0.1.2
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/data/builtins/ruby_core/range.yml +6 -4
- data/data/builtins/ruby_core/string.yml +15 -10
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +116 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +123 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +118 -0
- data/lib/rigor/analysis/check_rules.rb +346 -18
- data/lib/rigor/analysis/rule_catalog.rb +343 -0
- data/lib/rigor/analysis/runner.rb +2 -1
- data/lib/rigor/cli/diff_command.rb +169 -0
- data/lib/rigor/cli/explain_command.rb +129 -0
- data/lib/rigor/cli.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +18 -3
- data/lib/rigor/configuration.rb +10 -4
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +104 -12
- data/lib/rigor/inference/scope_indexer.rb +171 -2
- data/lib/rigor/plugin/io_boundary.rb +92 -19
- data/lib/rigor/plugin/trust_policy.rb +30 -7
- data/lib/rigor/scope.rb +30 -5
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/scope.rbs +3 -0
- metadata +7 -1
data/lib/rigor/cli.rb
CHANGED
|
@@ -22,7 +22,9 @@ module Rigor
|
|
|
22
22
|
"check" => :run_check,
|
|
23
23
|
"init" => :run_init,
|
|
24
24
|
"type-of" => :run_type_of,
|
|
25
|
-
"type-scan" => :run_type_scan
|
|
25
|
+
"type-scan" => :run_type_scan,
|
|
26
|
+
"explain" => :run_explain,
|
|
27
|
+
"diff" => :run_diff
|
|
26
28
|
}.freeze
|
|
27
29
|
|
|
28
30
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -207,6 +209,7 @@ module Rigor
|
|
|
207
209
|
# most likely to want to edit.
|
|
208
210
|
def init_template
|
|
209
211
|
<<~YAML
|
|
212
|
+
# yaml-language-server: $schema=https://github.com/zenwerk/rigor/raw/master/schemas/rigor-config.schema.json
|
|
210
213
|
# Rigor configuration. See docs/CURRENT_WORK.md for the
|
|
211
214
|
# full set of features the analyzer ships in this preview.
|
|
212
215
|
#
|
|
@@ -264,6 +267,18 @@ module Rigor
|
|
|
264
267
|
TypeScanCommand.new(argv: @argv, out: @out, err: @err).run
|
|
265
268
|
end
|
|
266
269
|
|
|
270
|
+
def run_explain
|
|
271
|
+
require_relative "cli/explain_command"
|
|
272
|
+
|
|
273
|
+
ExplainCommand.new(argv: @argv, out: @out, err: @err).run
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def run_diff
|
|
277
|
+
require_relative "cli/diff_command"
|
|
278
|
+
|
|
279
|
+
DiffCommand.new(argv: @argv, out: @out, err: @err).run
|
|
280
|
+
end
|
|
281
|
+
|
|
267
282
|
def write_result(result, format)
|
|
268
283
|
case format
|
|
269
284
|
when "json"
|
|
@@ -301,6 +316,8 @@ module Rigor
|
|
|
301
316
|
init Create a starter .rigor.yml
|
|
302
317
|
type-of Print the inferred type at FILE:LINE:COL
|
|
303
318
|
type-scan Report Scope#type_of coverage across PATHs
|
|
319
|
+
explain Print the description of one or all CheckRules
|
|
320
|
+
diff Compare current diagnostics to a saved baseline JSON
|
|
304
321
|
version Print the Rigor version
|
|
305
322
|
help Print this help
|
|
306
323
|
HELP
|
|
@@ -43,9 +43,14 @@ module Rigor
|
|
|
43
43
|
"call.argument-type-mismatch" => :warning,
|
|
44
44
|
"call.possible-nil-receiver" => :warning,
|
|
45
45
|
"flow.always-raises" => :warning,
|
|
46
|
+
"flow.unreachable-branch" => :info,
|
|
47
|
+
"flow.dead-assignment" => :info,
|
|
48
|
+
"flow.always-truthy-condition" => :info,
|
|
46
49
|
"assert.type-mismatch" => :error,
|
|
47
50
|
"dump.type" => :info,
|
|
48
|
-
"def.return-type-mismatch" => :warning
|
|
51
|
+
"def.return-type-mismatch" => :warning,
|
|
52
|
+
"def.method-visibility-mismatch" => :warning,
|
|
53
|
+
"def.ivar-write-mismatch" => :warning
|
|
49
54
|
}.freeze,
|
|
50
55
|
balanced: {
|
|
51
56
|
"call.undefined-method" => :error,
|
|
@@ -53,9 +58,14 @@ module Rigor
|
|
|
53
58
|
"call.argument-type-mismatch" => :error,
|
|
54
59
|
"call.possible-nil-receiver" => :error,
|
|
55
60
|
"flow.always-raises" => :error,
|
|
61
|
+
"flow.unreachable-branch" => :warning,
|
|
62
|
+
"flow.dead-assignment" => :warning,
|
|
63
|
+
"flow.always-truthy-condition" => :warning,
|
|
56
64
|
"assert.type-mismatch" => :error,
|
|
57
65
|
"dump.type" => :info,
|
|
58
|
-
"def.return-type-mismatch" => :warning
|
|
66
|
+
"def.return-type-mismatch" => :warning,
|
|
67
|
+
"def.method-visibility-mismatch" => :error,
|
|
68
|
+
"def.ivar-write-mismatch" => :warning
|
|
59
69
|
}.freeze,
|
|
60
70
|
strict: {
|
|
61
71
|
"call.undefined-method" => :error,
|
|
@@ -63,9 +73,14 @@ module Rigor
|
|
|
63
73
|
"call.argument-type-mismatch" => :error,
|
|
64
74
|
"call.possible-nil-receiver" => :error,
|
|
65
75
|
"flow.always-raises" => :error,
|
|
76
|
+
"flow.unreachable-branch" => :error,
|
|
77
|
+
"flow.dead-assignment" => :error,
|
|
78
|
+
"flow.always-truthy-condition" => :error,
|
|
66
79
|
"assert.type-mismatch" => :error,
|
|
67
80
|
"dump.type" => :error,
|
|
68
|
-
"def.return-type-mismatch" => :error
|
|
81
|
+
"def.return-type-mismatch" => :error,
|
|
82
|
+
"def.method-visibility-mismatch" => :error,
|
|
83
|
+
"def.ivar-write-mismatch" => :error
|
|
69
84
|
}.freeze
|
|
70
85
|
}.freeze
|
|
71
86
|
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -54,7 +54,8 @@ module Rigor
|
|
|
54
54
|
},
|
|
55
55
|
"plugins_io" => {
|
|
56
56
|
"network" => "disabled",
|
|
57
|
-
"allowed_paths" => []
|
|
57
|
+
"allowed_paths" => [],
|
|
58
|
+
"allowed_url_hosts" => []
|
|
58
59
|
},
|
|
59
60
|
"severity_profile" => "balanced",
|
|
60
61
|
"severity_overrides" => {}
|
|
@@ -70,6 +71,7 @@ module Rigor
|
|
|
70
71
|
attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :disabled_rules,
|
|
71
72
|
:libraries, :signature_paths, :fold_platform_specific_paths,
|
|
72
73
|
:plugins_io_network, :plugins_io_allowed_paths,
|
|
74
|
+
:plugins_io_allowed_url_hosts,
|
|
73
75
|
:severity_profile, :severity_overrides
|
|
74
76
|
|
|
75
77
|
# Loads a configuration file.
|
|
@@ -182,7 +184,8 @@ module Rigor
|
|
|
182
184
|
end
|
|
183
185
|
private_class_method :load_with_includes, :merge_includes, :resolve_paths_in, :deep_merge
|
|
184
186
|
|
|
185
|
-
|
|
187
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
188
|
+
def initialize(data = DEFAULTS)
|
|
186
189
|
cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
|
|
187
190
|
plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
|
|
188
191
|
|
|
@@ -203,6 +206,7 @@ module Rigor
|
|
|
203
206
|
@cache_path = cache.fetch("path").to_s
|
|
204
207
|
@plugins_io_network = coerce_network_policy(plugins_io.fetch("network"))
|
|
205
208
|
@plugins_io_allowed_paths = Array(plugins_io.fetch("allowed_paths")).map(&:to_s).freeze
|
|
209
|
+
@plugins_io_allowed_url_hosts = Array(plugins_io.fetch("allowed_url_hosts")).map(&:to_s).freeze
|
|
206
210
|
@severity_profile = coerce_severity_profile(
|
|
207
211
|
data.fetch("severity_profile", DEFAULTS.fetch("severity_profile"))
|
|
208
212
|
)
|
|
@@ -210,6 +214,7 @@ module Rigor
|
|
|
210
214
|
data.fetch("severity_overrides", DEFAULTS.fetch("severity_overrides"))
|
|
211
215
|
)
|
|
212
216
|
end
|
|
217
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
213
218
|
|
|
214
219
|
def to_h
|
|
215
220
|
{
|
|
@@ -226,7 +231,8 @@ module Rigor
|
|
|
226
231
|
},
|
|
227
232
|
"plugins_io" => {
|
|
228
233
|
"network" => plugins_io_network.to_s,
|
|
229
|
-
"allowed_paths" => plugins_io_allowed_paths
|
|
234
|
+
"allowed_paths" => plugins_io_allowed_paths,
|
|
235
|
+
"allowed_url_hosts" => plugins_io_allowed_url_hosts
|
|
230
236
|
},
|
|
231
237
|
"severity_profile" => severity_profile.to_s,
|
|
232
238
|
"severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] }
|
|
@@ -284,7 +290,7 @@ module Rigor
|
|
|
284
290
|
# `Configuration` does not require the plugin namespace at
|
|
285
291
|
# load time (Configuration is loaded before Plugin in
|
|
286
292
|
# `lib/rigor.rb`); the two stay in lockstep via spec.
|
|
287
|
-
VALID_NETWORK_POLICIES = %i[disabled].freeze
|
|
293
|
+
VALID_NETWORK_POLICIES = %i[disabled allowlist].freeze
|
|
288
294
|
private_constant :VALID_NETWORK_POLICIES
|
|
289
295
|
|
|
290
296
|
def coerce_network_policy(value)
|
|
@@ -8,24 +8,40 @@ module Rigor
|
|
|
8
8
|
module Inference
|
|
9
9
|
module MethodDispatcher
|
|
10
10
|
# Picks the RBS overload that should answer a call given the
|
|
11
|
-
# caller's actual argument types. Slice 4 phase 2c shape
|
|
11
|
+
# caller's actual argument types. Slice 4 phase 2c shape (with
|
|
12
|
+
# the v0.1.2 interface-strictness preference layered on top):
|
|
12
13
|
#
|
|
13
14
|
# 1. Filter overloads by positional arity (required, optional and
|
|
14
15
|
# rest_positionals are honored; required_keywords disqualify the
|
|
15
16
|
# overload because we do not yet thread keyword args through
|
|
16
17
|
# `call_arg_types`).
|
|
17
|
-
# 2.
|
|
18
|
-
# whose every (param, arg)
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
18
|
+
# 2. **Pass 1 — strict matches first.** Among the arity-matching
|
|
19
|
+
# overloads, prefer the first one whose every (param, arg)
|
|
20
|
+
# pair returns a `yes` or `maybe` answer AND whose param
|
|
21
|
+
# types do NOT translate through `RBS::Types::Alias` /
|
|
22
|
+
# `Interface` / `Intersection`. The translator demotes those
|
|
23
|
+
# to `Dynamic[Top]`, which gradually accepts any argument —
|
|
24
|
+
# so without this preference, an alias-typed overload like
|
|
25
|
+
# `Array#[](::int) -> Elem` would beat the strict
|
|
26
|
+
# `Array#[](Range) -> Array[Elem]?` overload for a Range
|
|
27
|
+
# argument. (Surfaced during v0.1.1 self-analysis; see the
|
|
28
|
+
# "Interface-strictness on overload selection" item in
|
|
29
|
+
# `docs/MILESTONES.md`.)
|
|
30
|
+
# 3. **Pass 2 — gradual fall-back.** If no fully strict overload
|
|
31
|
+
# matches, accept the first arity-and-gradual-accept match
|
|
32
|
+
# (the v0.1.1 behaviour). Alias / Interface / Intersection
|
|
33
|
+
# params still reach this pass, so call sites whose only
|
|
34
|
+
# candidate IS an alias-typed overload keep working.
|
|
35
|
+
# 4. If no overload matches at all, fall back to
|
|
36
|
+
# `method_types.first` so existing call sites keep their
|
|
37
|
+
# phase 1 / 2b behavior. This preserves the fail-soft
|
|
38
|
+
# invariant of the dispatcher.
|
|
23
39
|
#
|
|
24
40
|
# The selector is intentionally agnostic about the dispatch kind
|
|
25
41
|
# (instance vs singleton). Both kinds share the same arity and
|
|
26
42
|
# acceptance shape; the difference is only in which `Definition`
|
|
27
43
|
# the caller fetched.
|
|
28
|
-
module OverloadSelector
|
|
44
|
+
module OverloadSelector # rubocop:disable Metrics/ModuleLength
|
|
29
45
|
module_function
|
|
30
46
|
|
|
31
47
|
# @param method_definition [RBS::Definition::Method]
|
|
@@ -61,6 +77,18 @@ module Rigor
|
|
|
61
77
|
# compatibility.
|
|
62
78
|
param_overrides = RbsExtended.param_type_override_map(method_definition)
|
|
63
79
|
|
|
80
|
+
# Pass 1: prefer overloads whose param types stay strict —
|
|
81
|
+
# no translator-induced `Dynamic[Top]` from Alias /
|
|
82
|
+
# Interface / Intersection. The pass is skipped
|
|
83
|
+
# entirely when any arg is `Dynamic[Top]` (literally
|
|
84
|
+
# `untyped`), because gradual acceptance against an
|
|
85
|
+
# untyped arg accepts every param indiscriminately and
|
|
86
|
+
# would let pass 1 lock in an arbitrary strict overload
|
|
87
|
+
# (e.g. `Regexp#=~(nil) -> nil` over the
|
|
88
|
+
# `(::interned?) -> Integer?` overload). Pass 2 falls
|
|
89
|
+
# back to the original gradual matcher so overloads
|
|
90
|
+
# that legitimately rely on duck-typed params still
|
|
91
|
+
# resolve when nothing stricter applies.
|
|
64
92
|
match = find_matching_overload(
|
|
65
93
|
overloads,
|
|
66
94
|
arg_types: arg_types,
|
|
@@ -68,7 +96,17 @@ module Rigor
|
|
|
68
96
|
instance_type: instance_type,
|
|
69
97
|
type_vars: type_vars,
|
|
70
98
|
block_required: block_required,
|
|
71
|
-
param_overrides: param_overrides
|
|
99
|
+
param_overrides: param_overrides,
|
|
100
|
+
strict: true
|
|
101
|
+
) || find_matching_overload(
|
|
102
|
+
overloads,
|
|
103
|
+
arg_types: arg_types,
|
|
104
|
+
self_type: self_type,
|
|
105
|
+
instance_type: instance_type,
|
|
106
|
+
type_vars: type_vars,
|
|
107
|
+
block_required: block_required,
|
|
108
|
+
param_overrides: param_overrides,
|
|
109
|
+
strict: false
|
|
72
110
|
)
|
|
73
111
|
return match if match
|
|
74
112
|
return overloads.find { |mt| overload_has_block?(mt) } if block_required
|
|
@@ -84,11 +122,14 @@ module Rigor
|
|
|
84
122
|
class << self
|
|
85
123
|
private
|
|
86
124
|
|
|
87
|
-
# rubocop:disable Metrics/ParameterLists
|
|
125
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
88
126
|
def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
|
|
89
|
-
param_overrides:)
|
|
127
|
+
param_overrides:, strict:)
|
|
128
|
+
return nil if strict && arg_types.any? { |t| untyped_arg?(t) }
|
|
129
|
+
|
|
90
130
|
overloads.find do |method_type|
|
|
91
131
|
next false if block_required && !OverloadSelector.overload_has_block?(method_type)
|
|
132
|
+
next false if strict && !strictly_typed_params?(method_type, arg_types.size)
|
|
92
133
|
|
|
93
134
|
matches?(
|
|
94
135
|
method_type,
|
|
@@ -100,7 +141,58 @@ module Rigor
|
|
|
100
141
|
)
|
|
101
142
|
end
|
|
102
143
|
end
|
|
103
|
-
# rubocop:enable Metrics/ParameterLists
|
|
144
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
145
|
+
|
|
146
|
+
# Treats the literal `untyped` carrier (`Dynamic[Top]`)
|
|
147
|
+
# as too imprecise to drive a strict-pass match. Other
|
|
148
|
+
# `Dynamic`-wrapped types with a concrete static facet
|
|
149
|
+
# carry enough information to pick a sensible overload.
|
|
150
|
+
def untyped_arg?(type)
|
|
151
|
+
type.is_a?(Type::Dynamic) && type.static_facet.is_a?(Type::Top)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Returns true when every positional param the call
|
|
155
|
+
# site engages translates to a non-`Dynamic[Top]`
|
|
156
|
+
# carrier. Alias / Interface / Intersection RBS types
|
|
157
|
+
# all degrade to `Dynamic[Top]` per the translator's
|
|
158
|
+
# current shape — those gradually accept any arg, so
|
|
159
|
+
# an overload that includes one would beat strictly-
|
|
160
|
+
# typed alternatives in pass 2 of the selector.
|
|
161
|
+
def strictly_typed_params?(method_type, actual_count)
|
|
162
|
+
fun = method_type.type
|
|
163
|
+
return false unless arity_compatible?(fun, actual_count)
|
|
164
|
+
|
|
165
|
+
params = positional_params_for(fun, actual_count)
|
|
166
|
+
params.all? { |param| !alias_or_interface_param?(param.type) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Recursive: an Optional / Union wrapper is strict iff
|
|
170
|
+
# every member is strict. Type args of a ClassInstance
|
|
171
|
+
# are NOT walked — `Range[::int]` is a Range carrier
|
|
172
|
+
# at the param level; the alias only colours the
|
|
173
|
+
# element type, which is checked separately when the
|
|
174
|
+
# element is actually accessed.
|
|
175
|
+
#
|
|
176
|
+
# `RBS::Types::Bases::Any` (the explicit `untyped`
|
|
177
|
+
# keyword) is treated like Alias / Interface /
|
|
178
|
+
# Intersection — both translate to `Dynamic[Top]`,
|
|
179
|
+
# both gradually accept anything. A `(untyped) -> T`
|
|
180
|
+
# catch-all overload that comes after the strictly-
|
|
181
|
+
# typed ones must lose pass 1 so the typed overloads
|
|
182
|
+
# win when their param actually fits the arg.
|
|
183
|
+
def alias_or_interface_param?(rbs_type)
|
|
184
|
+
case rbs_type
|
|
185
|
+
when RBS::Types::Alias, RBS::Types::Interface,
|
|
186
|
+
RBS::Types::Intersection, RBS::Types::Bases::Any
|
|
187
|
+
true
|
|
188
|
+
when RBS::Types::Optional
|
|
189
|
+
alias_or_interface_param?(rbs_type.type)
|
|
190
|
+
when RBS::Types::Union
|
|
191
|
+
rbs_type.types.any? { |t| alias_or_interface_param?(t) }
|
|
192
|
+
else
|
|
193
|
+
false
|
|
194
|
+
end
|
|
195
|
+
end
|
|
104
196
|
|
|
105
197
|
# rubocop:disable Metrics/ParameterLists
|
|
106
198
|
def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:, param_overrides:)
|
|
@@ -106,6 +106,14 @@ module Rigor
|
|
|
106
106
|
discovered_def_nodes = build_discovered_def_nodes(root)
|
|
107
107
|
seeded_scope = seeded_scope.with_discovered_def_nodes(discovered_def_nodes)
|
|
108
108
|
|
|
109
|
+
# v0.1.2 — per-class table of method visibilities
|
|
110
|
+
# (`:public` / `:private` / `:protected`). The
|
|
111
|
+
# `def.method-visibility-mismatch` CheckRule consults
|
|
112
|
+
# the table to flag explicit-non-self calls to a
|
|
113
|
+
# private user method.
|
|
114
|
+
discovered_method_visibilities = build_discovered_method_visibilities(root)
|
|
115
|
+
seeded_scope = seeded_scope.with_discovered_method_visibilities(discovered_method_visibilities)
|
|
116
|
+
|
|
109
117
|
table = {}.compare_by_identity
|
|
110
118
|
table.default = seeded_scope
|
|
111
119
|
|
|
@@ -340,7 +348,8 @@ module Rigor
|
|
|
340
348
|
accumulator.transform_values(&:freeze).freeze
|
|
341
349
|
end
|
|
342
350
|
|
|
343
|
-
|
|
351
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
352
|
+
def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
|
|
344
353
|
return unless node.is_a?(Prism::Node)
|
|
345
354
|
|
|
346
355
|
case node
|
|
@@ -356,6 +365,12 @@ module Rigor
|
|
|
356
365
|
walk_methods(node.body, qualified_prefix, true, accumulator)
|
|
357
366
|
return
|
|
358
367
|
end
|
|
368
|
+
when Prism::ConstantWriteNode
|
|
369
|
+
if meta_new_block_body(node)
|
|
370
|
+
child_prefix = qualified_prefix + [node.name.to_s]
|
|
371
|
+
walk_methods(meta_new_block_body(node), child_prefix, false, accumulator)
|
|
372
|
+
return
|
|
373
|
+
end
|
|
359
374
|
when Prism::DefNode
|
|
360
375
|
record_def_method(node, qualified_prefix, in_singleton_class, accumulator)
|
|
361
376
|
return
|
|
@@ -370,6 +385,24 @@ module Rigor
|
|
|
370
385
|
walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
|
|
371
386
|
end
|
|
372
387
|
end
|
|
388
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
389
|
+
|
|
390
|
+
# v0.1.2 — when a `Const = Data.define(*sym) do ... end`
|
|
391
|
+
# / `Const = Struct.new(*sym) do ... end` constant write
|
|
392
|
+
# carries a block, the block body holds method overrides
|
|
393
|
+
# whose canonical class is `Const`. Returns the block body
|
|
394
|
+
# node (a `Prism::StatementsNode`) when the rvalue
|
|
395
|
+
# matches; nil otherwise. Used by `walk_methods` /
|
|
396
|
+
# `walk_def_nodes` to push `Const` onto the qualified
|
|
397
|
+
# prefix before recursing.
|
|
398
|
+
def meta_new_block_body(node)
|
|
399
|
+
return nil unless node.is_a?(Prism::ConstantWriteNode)
|
|
400
|
+
|
|
401
|
+
rvalue = node.value
|
|
402
|
+
return nil unless data_define_call?(rvalue) || struct_new_call?(rvalue)
|
|
403
|
+
|
|
404
|
+
rvalue.block&.body
|
|
405
|
+
end
|
|
373
406
|
|
|
374
407
|
def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
375
408
|
return if qualified_prefix.empty?
|
|
@@ -397,7 +430,8 @@ module Rigor
|
|
|
397
430
|
accumulator.transform_values(&:freeze).freeze
|
|
398
431
|
end
|
|
399
432
|
|
|
400
|
-
|
|
433
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
434
|
+
def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
|
|
401
435
|
return unless node.is_a?(Prism::Node)
|
|
402
436
|
|
|
403
437
|
case node
|
|
@@ -413,6 +447,12 @@ module Rigor
|
|
|
413
447
|
walk_def_nodes(node.body, qualified_prefix, true, accumulator)
|
|
414
448
|
return
|
|
415
449
|
end
|
|
450
|
+
when Prism::ConstantWriteNode
|
|
451
|
+
if meta_new_block_body(node)
|
|
452
|
+
child_prefix = qualified_prefix + [node.name.to_s]
|
|
453
|
+
walk_def_nodes(meta_new_block_body(node), child_prefix, false, accumulator)
|
|
454
|
+
return
|
|
455
|
+
end
|
|
416
456
|
when Prism::DefNode
|
|
417
457
|
record_def_node(node, qualified_prefix, in_singleton_class, accumulator)
|
|
418
458
|
return
|
|
@@ -422,6 +462,7 @@ module Rigor
|
|
|
422
462
|
walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
|
|
423
463
|
end
|
|
424
464
|
end
|
|
465
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
425
466
|
|
|
426
467
|
# v0.0.3 A — sentinel key under which `record_def_node`
|
|
427
468
|
# files DefNodes that live outside any class / module
|
|
@@ -440,6 +481,134 @@ module Rigor
|
|
|
440
481
|
accumulator[class_name][def_node.name] = def_node
|
|
441
482
|
end
|
|
442
483
|
|
|
484
|
+
VISIBILITY_MODIFIERS = %i[public private protected].freeze
|
|
485
|
+
|
|
486
|
+
# v0.1.2 — per-class method-visibility table for the
|
|
487
|
+
# `def.method-visibility-mismatch` CheckRule.
|
|
488
|
+
#
|
|
489
|
+
# Tracks two visibility-changing forms:
|
|
490
|
+
#
|
|
491
|
+
# - **Modifier blocks**: a bare `private` / `protected` /
|
|
492
|
+
# `public` call inside a class body switches the
|
|
493
|
+
# "current default" visibility for every subsequent
|
|
494
|
+
# `def` until another modifier flips it again.
|
|
495
|
+
# - **Named-argument form**: `private :foo, :bar` (or
|
|
496
|
+
# the same with `protected` / `public`) marks specific
|
|
497
|
+
# names already-recorded under the class. Symbol-only
|
|
498
|
+
# args are recognised; `private def foo; end` (the
|
|
499
|
+
# wrap-around form) is not yet — it would need
|
|
500
|
+
# tracking the def-call's return-value visibility,
|
|
501
|
+
# which is a separate slice.
|
|
502
|
+
#
|
|
503
|
+
# Top-level (no surrounding class) defs do not contribute
|
|
504
|
+
# — Ruby's top-level visibility nuances (private at
|
|
505
|
+
# top-level marks the method on `Object`) are out of
|
|
506
|
+
# scope for v0.1.2.
|
|
507
|
+
def build_discovered_method_visibilities(root)
|
|
508
|
+
accumulator = {}
|
|
509
|
+
walk_method_visibilities(root, [], false, :public, accumulator)
|
|
510
|
+
accumulator.transform_values(&:freeze).freeze
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
514
|
+
def walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
|
|
515
|
+
return current_visibility unless node.is_a?(Prism::Node)
|
|
516
|
+
|
|
517
|
+
case node
|
|
518
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
519
|
+
name = qualified_name_for(node.constant_path)
|
|
520
|
+
if name
|
|
521
|
+
child_prefix = qualified_prefix + [name]
|
|
522
|
+
walk_method_visibilities(node.body, child_prefix, false, :public, accumulator) if node.body
|
|
523
|
+
return current_visibility
|
|
524
|
+
end
|
|
525
|
+
when Prism::SingletonClassNode
|
|
526
|
+
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
527
|
+
walk_method_visibilities(node.body, qualified_prefix, true, :public, accumulator)
|
|
528
|
+
return current_visibility
|
|
529
|
+
end
|
|
530
|
+
when Prism::ConstantWriteNode
|
|
531
|
+
if meta_new_block_body(node)
|
|
532
|
+
child_prefix = qualified_prefix + [node.name.to_s]
|
|
533
|
+
walk_method_visibilities(meta_new_block_body(node), child_prefix, false, :public, accumulator)
|
|
534
|
+
return current_visibility
|
|
535
|
+
end
|
|
536
|
+
when Prism::DefNode
|
|
537
|
+
record_def_visibility(node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
|
|
538
|
+
return current_visibility
|
|
539
|
+
when Prism::CallNode
|
|
540
|
+
updated = apply_visibility_call(node, qualified_prefix, current_visibility, accumulator)
|
|
541
|
+
return updated unless updated.equal?(current_visibility)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Statement-position StatementsNode preserves
|
|
545
|
+
# left-to-right visibility flow; everything else
|
|
546
|
+
# recurses with the entry visibility unchanged.
|
|
547
|
+
if node.is_a?(Prism::StatementsNode)
|
|
548
|
+
local_visibility = current_visibility
|
|
549
|
+
node.compact_child_nodes.each do |child|
|
|
550
|
+
local_visibility = walk_method_visibilities(child, qualified_prefix, in_singleton_class,
|
|
551
|
+
local_visibility, accumulator)
|
|
552
|
+
end
|
|
553
|
+
else
|
|
554
|
+
node.compact_child_nodes.each do |child|
|
|
555
|
+
walk_method_visibilities(child, qualified_prefix, in_singleton_class, current_visibility, accumulator)
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
current_visibility
|
|
559
|
+
end
|
|
560
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
561
|
+
|
|
562
|
+
def record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
|
|
563
|
+
return if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
|
|
564
|
+
return if qualified_prefix.empty?
|
|
565
|
+
|
|
566
|
+
class_name = qualified_prefix.join("::")
|
|
567
|
+
accumulator[class_name] ||= {}
|
|
568
|
+
accumulator[class_name][def_node.name] = current_visibility
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Recognises modifier calls on the implicit-self receiver
|
|
572
|
+
# inside a class body. Returns the (possibly updated)
|
|
573
|
+
# current visibility:
|
|
574
|
+
#
|
|
575
|
+
# - `private` / `public` / `protected` (no args) —
|
|
576
|
+
# switch the running default for subsequent defs.
|
|
577
|
+
# - `private :foo, :bar` — back-patch the named methods
|
|
578
|
+
# in the accumulator. Returns `current_visibility`
|
|
579
|
+
# unchanged because the running default does NOT
|
|
580
|
+
# change for this form.
|
|
581
|
+
def apply_visibility_call(call_node, qualified_prefix, current_visibility, accumulator)
|
|
582
|
+
return current_visibility unless call_node.receiver.nil?
|
|
583
|
+
return current_visibility unless VISIBILITY_MODIFIERS.include?(call_node.name)
|
|
584
|
+
return current_visibility if qualified_prefix.empty?
|
|
585
|
+
|
|
586
|
+
args = call_node.arguments&.arguments || []
|
|
587
|
+
if args.empty?
|
|
588
|
+
call_node.name
|
|
589
|
+
else
|
|
590
|
+
apply_named_visibility(args, qualified_prefix, call_node.name, accumulator)
|
|
591
|
+
current_visibility
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def apply_named_visibility(args, qualified_prefix, visibility, accumulator)
|
|
596
|
+
class_name = qualified_prefix.join("::")
|
|
597
|
+
args.each do |arg|
|
|
598
|
+
name = visibility_target_name(arg)
|
|
599
|
+
next if name.nil?
|
|
600
|
+
|
|
601
|
+
accumulator[class_name] ||= {}
|
|
602
|
+
accumulator[class_name][name] = visibility
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def visibility_target_name(arg)
|
|
607
|
+
return arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
608
|
+
|
|
609
|
+
nil
|
|
610
|
+
end
|
|
611
|
+
|
|
443
612
|
# Registers the alias name in the `discovered_methods` table so
|
|
444
613
|
# `undefined-method` diagnostics are not emitted for calls to the
|
|
445
614
|
# aliased name. The kind mirrors the surrounding class context
|