rigortype 0.4.5 → 0.4.6
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 +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugins_command.rb +5 -0
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +4 -4
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -1040,9 +1040,10 @@ module Rigor
|
|
|
1040
1040
|
# No type carrier has a method that changes it once built (a built type is frozen; one `Marshal.load`
|
|
1041
1041
|
# restores is not, but nothing mutates it either), so a memoised key cannot go stale. The memo is weak
|
|
1042
1042
|
# in both key and value: it keeps neither the type nor the string alive, and an entry whose string
|
|
1043
|
-
# nothing else holds is dropped at the next GC. It is keyed by identity on purpose:
|
|
1044
|
-
# `Constant[
|
|
1045
|
-
# module's ivar, so a pool worker renders the key every time, as every
|
|
1043
|
+
# nothing else holds is dropped at the next GC. It is keyed by identity on purpose: two equal members
|
|
1044
|
+
# need not render alike (`Constant[1..2]` and `Constant[1..2.0]`), so equality is not a safe key. A
|
|
1045
|
+
# non-main Ractor may not read the module's ivar, so a pool worker renders the key every time, as every
|
|
1046
|
+
# run did before.
|
|
1046
1047
|
def sort_key(member)
|
|
1047
1048
|
return member.describe(:short) unless Ractor.main?
|
|
1048
1049
|
|
data/lib/rigor/type/constant.rb
CHANGED
|
@@ -79,7 +79,10 @@ module Rigor
|
|
|
79
79
|
# Scalar carriers without RBS Literal support (Float, Range, Rational, Complex, Regexp, Pathname)
|
|
80
80
|
# keep their pre-existing widen-to-class-name behaviour because RBS rejects their literal spellings
|
|
81
81
|
# as syntax errors. `Range` is generic (`Range[out E]`), and a bare `Range` fails `rbs validate`, so
|
|
82
|
-
# it carries the class of its endpoints, or `untyped` when both are nil (#1891).
|
|
82
|
+
# it carries the class of its endpoints, or `untyped` when both are nil (#1891). `Set[unchecked out A]` is
|
|
83
|
+
# generic too, and a set is mutable: the constant's elements are only what it held when it was built, and
|
|
84
|
+
# `x << "a"` adds a member they do not name. It erases to `Set[untyped]`, as an Array or Hash literal's
|
|
85
|
+
# element type is widened rather than pinned (#2177).
|
|
83
86
|
def erase_to_rbs
|
|
84
87
|
case value
|
|
85
88
|
when true then "true"
|
|
@@ -88,6 +91,7 @@ module Rigor
|
|
|
88
91
|
when Integer then value.to_s
|
|
89
92
|
when Symbol, String then value.inspect
|
|
90
93
|
when Range then "Range[#{range_element_rbs}]"
|
|
94
|
+
when ::Set then "Set[untyped]"
|
|
91
95
|
else value.class.name
|
|
92
96
|
end
|
|
93
97
|
end
|
|
@@ -96,13 +100,51 @@ module Rigor
|
|
|
96
100
|
|
|
97
101
|
include Rigor::Type::AcceptanceRouter
|
|
98
102
|
|
|
103
|
+
# Two carriers are equal when they pin the same value, which for a Float is stricter than Ruby's `==`
|
|
104
|
+
# (#1513): `0.0` and `-0.0` are distinct values (`1.0 / x` tells them apart), so `0.0 | -0.0` keeps both
|
|
105
|
+
# members, and a NaN equals another NaN, so `==` stays reflexive and a union never collects one NaN member
|
|
106
|
+
# per occurrence. A Complex compares its parts the same way.
|
|
99
107
|
def ==(other)
|
|
100
|
-
other.is_a?(Constant) && value.class == other.value.class && value
|
|
108
|
+
other.is_a?(Constant) && value.class == other.value.class && Constant.same_value?(value, other.value)
|
|
101
109
|
end
|
|
102
110
|
alias eql? ==
|
|
103
111
|
|
|
104
112
|
def hash
|
|
105
|
-
[Constant, value.class, value].hash
|
|
113
|
+
[Constant, value.class, Constant.value_key(value)].hash
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# True when `left` and `right` (of one class) are the same value in the sense of {#==}.
|
|
117
|
+
def self.same_value?(left, right)
|
|
118
|
+
case left
|
|
119
|
+
when Float then same_float?(left, right)
|
|
120
|
+
when Complex then same_float?(left.real, right.real) && same_float?(left.imaginary, right.imaginary)
|
|
121
|
+
else left == right
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.same_float?(left, right)
|
|
126
|
+
return left == right unless left.is_a?(Float) && right.is_a?(Float)
|
|
127
|
+
return right.nan? if left.nan?
|
|
128
|
+
|
|
129
|
+
left.eql?(right) && (!left.zero? || (1.0 / left).positive? == (1.0 / right).positive?)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# A key whose `eql?` / `hash` agree with {.same_value?}: `uniq { |v| Constant.value_key(v) }` keeps
|
|
133
|
+
# `0.0` and `-0.0` apart and folds every NaN into one. Any other value is its own key, so plain `uniq`
|
|
134
|
+
# semantics are unchanged for it.
|
|
135
|
+
def self.value_key(value)
|
|
136
|
+
case value
|
|
137
|
+
when Float then float_key(value)
|
|
138
|
+
when Complex then [Complex, float_key(value.real), float_key(value.imaginary)]
|
|
139
|
+
else value
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.float_key(value)
|
|
144
|
+
return value unless value.is_a?(Float)
|
|
145
|
+
return :nan if value.nan?
|
|
146
|
+
|
|
147
|
+
value.zero? ? [Float, (1.0 / value)] : value
|
|
106
148
|
end
|
|
107
149
|
|
|
108
150
|
def inspect
|
|
@@ -29,7 +29,7 @@ module Rigor
|
|
|
29
29
|
EXTRA_KEY_POLICIES = %i[open closed].freeze
|
|
30
30
|
POLICY_KEYWORDS = %i[required_keys optional_keys read_only_keys extra_keys].freeze
|
|
31
31
|
# A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#erase_to_rbs};
|
|
32
|
-
# anything else
|
|
32
|
+
# anything else is a quoted Symbol with a fat arrow (`:"data-contrast" =>`). See {#erase_key_prefix}.
|
|
33
33
|
BARE_RECORD_KEY = /\A[A-Za-z_][A-Za-z0-9_]*[?!]?\z/
|
|
34
34
|
|
|
35
35
|
attr_reader :pairs, :required_keys, :optional_keys, :read_only_keys, :extra_keys
|
|
@@ -72,7 +72,7 @@ module Rigor
|
|
|
72
72
|
def erase_to_rbs
|
|
73
73
|
return "{}" if pairs.empty? && closed?
|
|
74
74
|
return hash_erasure unless closed?
|
|
75
|
-
return hash_erasure if pairs.each_key.any? { |k| !k.is_a?(Symbol) }
|
|
75
|
+
return hash_erasure if pairs.each_key.any? { |k| !k.is_a?(Symbol) || !record_key_spellable?(k) }
|
|
76
76
|
|
|
77
77
|
rendered = pairs.map { |k, v| "#{erase_key_prefix(k)} #{v.erase_to_rbs}" }
|
|
78
78
|
"{ #{rendered.join(', ')} }"
|
|
@@ -221,21 +221,26 @@ module Rigor
|
|
|
221
221
|
|
|
222
222
|
# An RBS record entry's key + separator, for the {#erase_to_rbs} path (which only ever sees
|
|
223
223
|
# Symbol keys — string-keyed shapes degrade to `Hash[...]` before reaching here). A Symbol whose
|
|
224
|
-
# text is a plain identifier is a bare key with a colon (`lang
|
|
225
|
-
# Symbol
|
|
226
|
-
# non-identifier (`data-contrast:`) and a quoted key with a colon
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
# form, so only genuinely non-identifier keys change. Optional keys carry a leading `?`.
|
|
224
|
+
# text is a plain identifier is a bare key with a colon (`lang:`, Ruby keywords like `class`
|
|
225
|
+
# included). Any other Symbol is a quoted Symbol literal with a fat arrow (`:"data-contrast" =>`):
|
|
226
|
+
# RBS rejects a bare non-identifier (`data-contrast:`) and a quoted key with a colon
|
|
227
|
+
# (`"data-contrast":`), and reads `"data-contrast" =>` as the String key, so the written record
|
|
228
|
+
# declared a key the body never has (#2176). Optional keys carry a leading `?`.
|
|
230
229
|
def erase_key_prefix(key)
|
|
231
230
|
optional = optional_key?(key) ? "?" : ""
|
|
232
231
|
if BARE_RECORD_KEY.match?(key.to_s)
|
|
233
232
|
"#{optional}#{key}:"
|
|
234
233
|
else
|
|
235
|
-
"#{optional}
|
|
234
|
+
"#{optional}:#{key.to_s.inspect} =>"
|
|
236
235
|
end
|
|
237
236
|
end
|
|
238
237
|
|
|
238
|
+
# `String#inspect` escapes `#{`, `#$` and `#@` as `\#`, which RBS's parser does not read back as
|
|
239
|
+
# `#`. A key spelled that way would name a different Symbol, so the shape erases to `Hash[...]`.
|
|
240
|
+
def record_key_spellable?(key)
|
|
241
|
+
!key.to_s.inspect.include?("\\#")
|
|
242
|
+
end
|
|
243
|
+
|
|
239
244
|
def hash_erasure
|
|
240
245
|
return "Hash[top, top]" if open?
|
|
241
246
|
return "Hash" if pairs.empty?
|
|
@@ -63,10 +63,11 @@ module Rigor
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# Erases to the tagging class nominal (conservative: the structural members are not RBS-expressible
|
|
66
|
-
# as a class instance). The anonymous case erases to the `Struct` supertype
|
|
66
|
+
# as a class instance). The anonymous case erases to the `Struct` supertype with an `untyped`
|
|
67
|
+
# element: `Struct[Elem]` is generic, and a bare `Struct` fails `rbs validate` (#2177).
|
|
67
68
|
def erase_to_rbs
|
|
68
69
|
name = class_name
|
|
69
|
-
return "Struct" if name.nil?
|
|
70
|
+
return "Struct[untyped]" if name.nil?
|
|
70
71
|
|
|
71
72
|
name
|
|
72
73
|
end
|
data/lib/rigor/version.rb
CHANGED
|
@@ -216,9 +216,10 @@ module Rigor
|
|
|
216
216
|
# - `initialize`
|
|
217
217
|
# - methods starting with `_` (Ruby convention for private/internal)
|
|
218
218
|
# - `def self.<name>` (singleton-side)
|
|
219
|
-
# - methods
|
|
220
|
-
#
|
|
221
|
-
#
|
|
219
|
+
# - methods that are not public at the end of the class body, read in source order: after a bare
|
|
220
|
+
# `private` / `protected` (until a `public` switches back), named by `private :foo` /
|
|
221
|
+
# `protected :foo`, or defined inline (`private def foo`) — Rails builds `action_methods` from the
|
|
222
|
+
# public instance methods. `public :foo` after a `protected` section makes `foo` an action again.
|
|
222
223
|
# - methods named as a callback target (`before_action :name`, `after_action`, `around_action`)
|
|
223
224
|
#
|
|
224
225
|
# Pre-fix, Mastodon's `AdminMailer#process_params` / `set_instance` / `set_locale` /
|
|
@@ -227,64 +228,80 @@ module Rigor
|
|
|
227
228
|
CALLBACK_DECLARATIONS = %i[before_action after_action around_action].freeze
|
|
228
229
|
private_constant :CALLBACK_DECLARATIONS
|
|
229
230
|
|
|
231
|
+
VISIBILITY_KEYWORDS = %i[public private protected].freeze
|
|
232
|
+
private_constant :VISIBILITY_KEYWORDS
|
|
233
|
+
|
|
230
234
|
def collect_action_defs(body)
|
|
231
235
|
return [] if body.nil?
|
|
232
236
|
|
|
233
|
-
|
|
234
|
-
|
|
237
|
+
callback_names = collect_callback_names(body)
|
|
238
|
+
def_nodes, final_visibility = scan_visibility(body)
|
|
235
239
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
next
|
|
239
|
-
next
|
|
240
|
-
next
|
|
241
|
-
next [] if node.name.to_s.start_with?("_")
|
|
242
|
-
next [] if visibility == :private
|
|
243
|
-
next [] if private_names.include?(node.name)
|
|
244
|
-
next [] if callback_names.include?(node.name)
|
|
240
|
+
def_nodes.select do |node|
|
|
241
|
+
next false if node.receiver.is_a?(Prism::SelfNode)
|
|
242
|
+
next false if node.name == :initialize
|
|
243
|
+
next false if node.name.to_s.start_with?("_")
|
|
244
|
+
next false if callback_names.include?(node.name)
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
# Rails builds `action_methods` from the PUBLIC instance methods, so only a method whose final
|
|
247
|
+
# visibility is public is an action.
|
|
248
|
+
final_visibility[node.name] == :public
|
|
247
249
|
end
|
|
248
250
|
end
|
|
249
251
|
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def collect_visibility_and_callbacks(body)
|
|
254
|
-
private_names = []
|
|
255
|
-
callback_names = []
|
|
256
|
-
|
|
252
|
+
# Symbol arguments to callback declarations (`before_action :setup`, etc.).
|
|
253
|
+
def collect_callback_names(body)
|
|
254
|
+
names = []
|
|
257
255
|
body.rigor_each_child do |node|
|
|
258
|
-
next unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
256
|
+
next unless node.is_a?(Prism::CallNode) && node.receiver.nil? && CALLBACK_DECLARATIONS.include?(node.name)
|
|
259
257
|
|
|
260
|
-
|
|
261
|
-
Rigor::Source::Literals.symbol(arg)
|
|
258
|
+
(node.arguments&.arguments || []).each do |arg|
|
|
259
|
+
symbol = Rigor::Source::Literals.symbol(arg)
|
|
260
|
+
names << symbol if symbol
|
|
262
261
|
end
|
|
263
|
-
|
|
262
|
+
end
|
|
263
|
+
names.to_set
|
|
264
|
+
end
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
# Walks the class body in source order and returns `[def_nodes, final_visibility]`: every `def` the body
|
|
267
|
+
# spells (a direct statement or the inline argument of a visibility call, `private def x`), and the
|
|
268
|
+
# visibility each method name ends with. A bare `public` / `private` / `protected` sets the default for
|
|
269
|
+
# the defs after it; `private :x` / `public :x` / `protected def x` set that one method, whichever side of
|
|
270
|
+
# its `def` they fall on in the source, and the last statement to name a method wins.
|
|
271
|
+
def scan_visibility(body)
|
|
272
|
+
def_nodes = []
|
|
273
|
+
final_visibility = {}
|
|
274
|
+
default = :public
|
|
275
|
+
|
|
276
|
+
body.compact_child_nodes.each do |node|
|
|
277
|
+
case node
|
|
278
|
+
when Prism::DefNode
|
|
279
|
+
def_nodes << node
|
|
280
|
+
final_visibility[node.name] = default
|
|
281
|
+
when Prism::CallNode
|
|
282
|
+
default = apply_visibility_call(node, default, def_nodes, final_visibility)
|
|
268
283
|
end
|
|
269
284
|
end
|
|
270
285
|
|
|
271
|
-
[
|
|
286
|
+
[def_nodes, final_visibility]
|
|
272
287
|
end
|
|
273
288
|
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
289
|
+
# Applies one receiverless `public` / `private` / `protected` call and returns the (possibly new) default.
|
|
290
|
+
def apply_visibility_call(node, default, def_nodes, final_visibility)
|
|
291
|
+
return default unless node.receiver.nil? && VISIBILITY_KEYWORDS.include?(node.name)
|
|
292
|
+
|
|
293
|
+
args = node.arguments&.arguments || []
|
|
294
|
+
return node.name if args.empty?
|
|
295
|
+
|
|
296
|
+
args.flat_map { |arg| arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg] }.each do |arg|
|
|
297
|
+
if arg.is_a?(Prism::DefNode)
|
|
298
|
+
def_nodes << arg
|
|
299
|
+
final_visibility[arg.name] = node.name
|
|
300
|
+
elsif (name = Rigor::Source::Literals.symbol_or_string(arg))
|
|
301
|
+
final_visibility[name] = node.name
|
|
302
|
+
end
|
|
287
303
|
end
|
|
304
|
+
default
|
|
288
305
|
end
|
|
289
306
|
|
|
290
307
|
def build_class_entry(class_name, file_path, def_nodes, includes = [], module_actions = {})
|
|
@@ -52,13 +52,32 @@ module Rigor
|
|
|
52
52
|
# Bumped 2026-09-02 (#621) — mailer keys are de-rooted at the producer and a reopened class's
|
|
53
53
|
# actions are UNIONed rather than clobbered, so a cached 0.4.0 index can be missing actions this
|
|
54
54
|
# version knows about.
|
|
55
|
-
|
|
55
|
+
#
|
|
56
|
+
# Bumped 2026-10-23 (#2308) — a method after a bare `protected` is no longer an action, so a cached 0.5.0
|
|
57
|
+
# index can list protected helpers as actions (and as missing-view candidates).
|
|
58
|
+
version: "0.5.1",
|
|
56
59
|
description: "Validates ActionMailer call shape and view template existence.",
|
|
57
60
|
config_schema: {
|
|
58
61
|
"mailer_search_paths" => { kind: :array, default: ["app/mailers"] },
|
|
59
62
|
"mailer_base_classes" => { kind: :array, default: %w[ApplicationMailer ActionMailer::Base] },
|
|
60
63
|
"views_root" => { kind: :string, default: "app/views" }
|
|
61
64
|
},
|
|
65
|
+
# ADR-125 (#575, #2308) — a mailer instance serves exactly one action (`Mailer.welcome(...)` builds a
|
|
66
|
+
# fresh instance and runs that one method), so ivar writes in two different public actions are never live
|
|
67
|
+
# together and `def.ivar-write-mismatch` stops comparing them. `ActionMailer::Base` has no signature here;
|
|
68
|
+
# the rule matches an undeclared superclass by its written name.
|
|
69
|
+
per_invocation_classes: ["ActionMailer::Base"],
|
|
70
|
+
# A public method these macros name is a callback or a view helper, not an action: it runs inside
|
|
71
|
+
# whichever action triggers it (AbstractController::Callbacks / Helpers), so its writes stay compared with
|
|
72
|
+
# every action's. The actionpack list, plus the `*_deliver` callbacks (ActionMailer::Callbacks), which run
|
|
73
|
+
# on the action's own instance.
|
|
74
|
+
per_invocation_non_action_macros: %i[
|
|
75
|
+
before_action prepend_before_action append_before_action skip_before_action
|
|
76
|
+
after_action prepend_after_action append_after_action skip_after_action
|
|
77
|
+
around_action prepend_around_action append_around_action skip_around_action
|
|
78
|
+
before_deliver after_deliver around_deliver
|
|
79
|
+
helper_method
|
|
80
|
+
],
|
|
62
81
|
# ADR-103 WD10 (#387) — see {Effects} for what each row is and why.
|
|
63
82
|
effect_root: "rails",
|
|
64
83
|
effect_labels: ["rails.actionmailer.deliver"],
|
|
@@ -146,6 +146,19 @@ module Rigor
|
|
|
146
146
|
# `sig/action_view.rbs` so the name resolves, open so the helper surface stays lenient.
|
|
147
147
|
"ActionView::Base"
|
|
148
148
|
],
|
|
149
|
+
# ADR-125 (#575) — Rails instantiates a fresh controller per request and runs exactly one action on
|
|
150
|
+
# it, so ivar writes in two different public actions are never live together and
|
|
151
|
+
# `def.ivar-write-mismatch` stops comparing them. Neither root has a signature (see `sig/action_controller.rbs`
|
|
152
|
+
# for why); the rule matches an undeclared superclass by its written name.
|
|
153
|
+
per_invocation_classes: ["ActionController::Base", "ActionController::API"],
|
|
154
|
+
# A public method these macros name is a callback or a view helper, not an action: it runs inside
|
|
155
|
+
# whichever action triggers it, so its writes stay compared with every action's (#575's A1 decision).
|
|
156
|
+
per_invocation_non_action_macros: %i[
|
|
157
|
+
before_action prepend_before_action append_before_action skip_before_action
|
|
158
|
+
after_action prepend_after_action append_after_action skip_after_action
|
|
159
|
+
around_action prepend_around_action append_around_action skip_around_action
|
|
160
|
+
helper_method
|
|
161
|
+
],
|
|
149
162
|
# ADR-103 WD10 / WD14 (#387) — see {Effects} for what each row is and why.
|
|
150
163
|
effect_root: "rails",
|
|
151
164
|
effect_labels: %w[
|
|
@@ -97,8 +97,10 @@ module Rigor
|
|
|
97
97
|
|
|
98
98
|
# Cached separately from `:job_index` because the two invalidate on different files: editing a
|
|
99
99
|
# schedule changes which jobs are reached without touching `app/jobs` at all. The `watch:` roots the
|
|
100
|
-
# glob at the working directory so that CREATING a schedule file — not just editing one — is seen.
|
|
101
|
-
|
|
100
|
+
# glob at the working directory so that CREATING a schedule file — not just editing one — is seen. Each
|
|
101
|
+
# configured path is escaped to a literal name, so `recurring[1].yml` is watched as that file.
|
|
102
|
+
producer :recurring_jobs,
|
|
103
|
+
watch: -> { [[".", *@recurring_paths.map { |path| Rigor::RootedGlob.literal(path) }]] } do |_params|
|
|
102
104
|
RecurringScan.new(
|
|
103
105
|
io_boundary: io_boundary,
|
|
104
106
|
recurring_paths: @recurring_paths
|
|
@@ -33,9 +33,11 @@ module Rigor
|
|
|
33
33
|
# - `:authenticatable` — always-included base module
|
|
34
34
|
#
|
|
35
35
|
# The substrate's pre-pass scanner consults each module's RBS via `Environment::RbsLoader#instance_definition`
|
|
36
|
-
# to enumerate method names
|
|
37
|
-
#
|
|
38
|
-
#
|
|
36
|
+
# to enumerate method names, so it names only what the project's RBS declares. The `devise` gem ships no
|
|
37
|
+
# RBS and Rigor bundles none for `Devise::Models::*`: a project supplies those signatures (`signature_paths:`
|
|
38
|
+
# or an `rbs collection`), and only then do `valid_password?`, `send_reset_password_instructions`, etc.
|
|
39
|
+
# resolve through the synthetic-method tier. A name the model defines itself, or a project module it
|
|
40
|
+
# includes or prepends defines, is left to the project's definition.
|
|
39
41
|
#
|
|
40
42
|
# ## Precision tier
|
|
41
43
|
#
|
|
@@ -120,16 +120,17 @@ module Rigor
|
|
|
120
120
|
|
|
121
121
|
# The `watch:` glob tuples covering the scan's entire input — one `[root, pattern]` per project `paths:`
|
|
122
122
|
# entry. A directory entry watches its whole `**/*.rb` subtree; a single-file `.rb` entry watches
|
|
123
|
-
# exactly that file (`[dir, basename]`
|
|
124
|
-
# `Plugin::Base#watch_glob_entries`, so a relative CLI path
|
|
125
|
-
# tree from the project root on the next run. Co-extensive
|
|
126
|
-
# would read is watched, so any edit / addition / removal
|
|
123
|
+
# exactly that file (`[dir, basename]`, with the basename escaped so `[`, `{`, `*` or `?` match literally).
|
|
124
|
+
# Roots are expanded to absolute paths by `Plugin::Base#watch_glob_entries`, so a relative CLI path
|
|
125
|
+
# (`app/models`) re-validates against the same tree from the project root on the next run. Co-extensive
|
|
126
|
+
# with {#scannable_paths}: every `.rb` the scan would read is watched, so any edit / addition / removal
|
|
127
|
+
# under those paths invalidates the cache.
|
|
127
128
|
def alias_watch_globs
|
|
128
129
|
services.configuration.paths.filter_map do |entry|
|
|
129
130
|
if io_boundary.directory?(entry)
|
|
130
131
|
[entry, "**/*.rb"]
|
|
131
132
|
elsif io_boundary.file?(entry) && entry.end_with?(".rb")
|
|
132
|
-
[File.dirname(entry), File.basename(entry)]
|
|
133
|
+
[File.dirname(entry), Rigor::RootedGlob.literal(File.basename(entry))]
|
|
133
134
|
end
|
|
134
135
|
end
|
|
135
136
|
end
|
|
@@ -35,7 +35,8 @@ module Rigor
|
|
|
35
35
|
# LocalVariableReadNode. Composite receivers (`expect(foo.bar)`, `expect { ... }.to raise_error`)
|
|
36
36
|
# fall through.
|
|
37
37
|
# 3. The matcher is one of the recognised forms above (`be_a` / `be_kind_of` / `be_instance_of` /
|
|
38
|
-
# `be_an_instance_of` / `be_nil` / `eq(literal)` / `eql(literal)` / `match(/regex/)
|
|
38
|
+
# `be_an_instance_of` / `be_nil` / `eq(literal)` / `eql(literal)` / `match(/regex/)`, the last a meet
|
|
39
|
+
# with the local's members; see `regex_match_type`).
|
|
39
40
|
#
|
|
40
41
|
# ## Equality is the local's own method
|
|
41
42
|
#
|
|
@@ -152,10 +153,7 @@ module Rigor
|
|
|
152
153
|
when :eq, :eql
|
|
153
154
|
equality_type_for_literal_arg(matcher, target_local, negative: negative, scope: scope)
|
|
154
155
|
when :match
|
|
155
|
-
|
|
156
|
-
# through — the broader matcher dispatch needs the receiver to be a String, but we can only
|
|
157
|
-
# assert that for a literal regex.
|
|
158
|
-
string_type_for_regex_arg(matcher)
|
|
156
|
+
regex_match_type(matcher, target_local, negative: negative, scope: scope)
|
|
159
157
|
end
|
|
160
158
|
end
|
|
161
159
|
|
|
@@ -293,15 +291,108 @@ module Rigor
|
|
|
293
291
|
end
|
|
294
292
|
end
|
|
295
293
|
|
|
296
|
-
|
|
294
|
+
# --- `match(/regex/)` (#2033) ---
|
|
295
|
+
#
|
|
296
|
+
# RSpec's `match` passes when `expected === actual`, when `actual == expected`, or when `actual` responds to
|
|
297
|
+
# `match` and `actual.match(expected)` is truthy. `Regexp#===` matches a Symbol by its name, so a Symbol
|
|
298
|
+
# passes too, and keeps being a Symbol. The positive fact is therefore a meet with the local's members, as
|
|
299
|
+
# `eq`'s is: a String or Symbol member is kept as it is, and a member of a core class that has no `match` or
|
|
300
|
+
# `to_str` and whose `==` never equals a Regexp (`nil`, `true`, `false`, the numerics) is dropped, unless the
|
|
301
|
+
# project defines one of those methods on it or a core ancestor. An `Object`, `BasicObject`, `Kernel` or
|
|
302
|
+
# `Comparable` member reads `String | Symbol` ({#module_match_member}); any other module means no fact. Any other member (a user class, `Dynamic`) may pass, and
|
|
303
|
+
# the analyzer emits no fact; so does a subject none of whose members can pass (a `nil` local stays `nil`). The negative form emits none either: a String that does not match passes
|
|
304
|
+
# `not_to match`, so nothing leaves the local.
|
|
305
|
+
REGEX_MATCH_KEPT_CLASSES = %w[String Symbol].freeze
|
|
306
|
+
REGEX_MATCH_METHODS = %i[match to_str].freeze
|
|
307
|
+
Ractor.make_shareable(REGEX_MATCH_KEPT_CLASSES)
|
|
308
|
+
Ractor.make_shareable(REGEX_MATCH_METHODS)
|
|
309
|
+
private_constant :REGEX_MATCH_KEPT_CLASSES, :REGEX_MATCH_METHODS
|
|
310
|
+
|
|
311
|
+
def regex_match_type(matcher, target_local, negative:, scope:)
|
|
312
|
+
return nil if negative || scope.nil?
|
|
313
|
+
return nil unless regex_literal_arg?(matcher)
|
|
314
|
+
|
|
315
|
+
current = scope.local(target_local)
|
|
316
|
+
return nil if current.nil?
|
|
317
|
+
|
|
318
|
+
members = current.is_a?(Rigor::Type::Union) ? current.members : [current]
|
|
319
|
+
kept = []
|
|
320
|
+
members.each do |member|
|
|
321
|
+
met = regex_match_member(member, scope)
|
|
322
|
+
return nil if met.equal?(UNDECIDED)
|
|
323
|
+
|
|
324
|
+
kept << met unless met.nil?
|
|
325
|
+
end
|
|
326
|
+
kept.empty? ? nil : Rigor::Type::Combinator.union(*kept)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def regex_literal_arg?(matcher)
|
|
297
330
|
args = matcher.arguments&.arguments || []
|
|
298
|
-
|
|
331
|
+
args.size == 1 &&
|
|
332
|
+
(args.first.is_a?(Prism::RegularExpressionNode) || args.first.is_a?(Prism::InterpolatedRegularExpressionNode))
|
|
333
|
+
end
|
|
299
334
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
335
|
+
# The member itself when it may pass, nil when it cannot, UNDECIDED when Rigor cannot tell.
|
|
336
|
+
def regex_match_member(member, scope)
|
|
337
|
+
module_member = module_match_member(member, scope)
|
|
338
|
+
return module_member unless module_member.nil?
|
|
303
339
|
|
|
304
|
-
|
|
340
|
+
class_name = member_class_name(member)
|
|
341
|
+
return UNDECIDED if class_name.nil?
|
|
342
|
+
return member if REGEX_MATCH_KEPT_CLASSES.include?(class_name)
|
|
343
|
+
return member if project_defines?(class_name, :==, scope)
|
|
344
|
+
return member if REGEX_MATCH_METHODS.any? { |name| project_defines_on_chain?(class_name, name, scope) }
|
|
345
|
+
|
|
346
|
+
nil
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
OPEN_SURFACE_CLASS_NAMES = %w[Object BasicObject].freeze
|
|
350
|
+
# The modules String's or Symbol's own ancestry includes (`Kernel`, `Comparable`), read from the running Ruby.
|
|
351
|
+
STRING_SYMBOL_MODULES = (String.ancestors + Symbol.ancestors).grep_v(Class).filter_map(&:name).uniq.freeze
|
|
352
|
+
Ractor.make_shareable(OPEN_SURFACE_CLASS_NAMES)
|
|
353
|
+
Ractor.make_shareable(STRING_SYMBOL_MODULES)
|
|
354
|
+
private_constant :OPEN_SURFACE_CLASS_NAMES, :STRING_SYMBOL_MODULES
|
|
355
|
+
|
|
356
|
+
# The meet for an `Object`, `BasicObject` or module member, or nil for any other member. Its instances may be
|
|
357
|
+
# anything, a `to_str` or `match` object included. A member that declares `match` or `to_str` itself (its
|
|
358
|
+
# signature, a core ancestor, or the project's source) is kept whole. `Object`, `BasicObject` and a module
|
|
359
|
+
# String or Symbol includes otherwise read `String | Symbol`, as the String they most often are, so a String
|
|
360
|
+
# method called after the matcher reports nothing. Any other module is UNDECIDED: a passing value may include
|
|
361
|
+
# it without being a String. (`approval-queue.d/regexp-match-open-surface-subject.md`, shared with #2256.)
|
|
362
|
+
def module_match_member(member, scope)
|
|
363
|
+
return nil unless member.is_a?(Rigor::Type::Nominal) && member.type_args.empty?
|
|
364
|
+
|
|
365
|
+
name = member.class_name
|
|
366
|
+
string_like = OPEN_SURFACE_CLASS_NAMES.include?(name) || STRING_SYMBOL_MODULES.include?(name)
|
|
367
|
+
return nil unless string_like || scope.environment&.rbs_module?(name)
|
|
368
|
+
return member if REGEX_MATCH_METHODS.any? { |method_name| declares_method?(name, method_name, scope) }
|
|
369
|
+
return UNDECIDED unless string_like
|
|
370
|
+
|
|
371
|
+
Rigor::Type::Combinator.union(Rigor::Type::Combinator.nominal_of("String"),
|
|
372
|
+
Rigor::Type::Combinator.nominal_of("Symbol"))
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def declares_method?(name, method_name, scope)
|
|
376
|
+
project_defines_on_chain?(name, method_name, scope) ||
|
|
377
|
+
!Rigor::Reflection.instance_method_definition(name, method_name, scope: scope).nil?
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Each core class's (and `Object` / `BasicObject` / `Kernel`'s) runtime ancestor chain. The project's
|
|
381
|
+
# reopenings are looked up along it by name, since the scope's ancestry walk does not cross core classes:
|
|
382
|
+
# `class Numeric; def to_str` reaches a Float member, `module Kernel; def match` a nil one.
|
|
383
|
+
CORE_ANCESTOR_CHAINS = [*EQUALITY_CORE_CLASSES, Object, BasicObject, Kernel].to_h do |mod|
|
|
384
|
+
[mod.name, mod.ancestors.filter_map(&:name).freeze]
|
|
385
|
+
end.freeze
|
|
386
|
+
Ractor.make_shareable(CORE_ANCESTOR_CHAINS)
|
|
387
|
+
private_constant :CORE_ANCESTOR_CHAINS
|
|
388
|
+
|
|
389
|
+
# Whether the project's source defines `method_name` on `class_name` or an ancestor, core ones included: a
|
|
390
|
+
# `def`, an included module, or a `define_method`.
|
|
391
|
+
def project_defines_on_chain?(class_name, method_name, scope)
|
|
392
|
+
CORE_ANCESTOR_CHAINS.fetch(class_name, [class_name]).any? do |name|
|
|
393
|
+
!scope.user_def_through_ancestors(name, method_name).first.nil? ||
|
|
394
|
+
scope.discovered_method_through_ancestors?(name, method_name, :instance)
|
|
395
|
+
end
|
|
305
396
|
end
|
|
306
397
|
|
|
307
398
|
NO_LITERAL = Object.new.freeze
|
|
@@ -97,8 +97,10 @@ module Rigor
|
|
|
97
97
|
|
|
98
98
|
# Cached separately from `:worker_index` because the two invalidate on different files: editing a
|
|
99
99
|
# schedule changes which workers are reached without touching `app/workers` at all. The `watch:` roots
|
|
100
|
-
# the glob at the working directory so that CREATING a schedule file — not just editing one — is seen.
|
|
101
|
-
|
|
100
|
+
# the glob at the working directory so that CREATING a schedule file — not just editing one — is seen. Each
|
|
101
|
+
# configured path is escaped to a literal name, so `sidekiq[1].yml` is watched as that file.
|
|
102
|
+
producer :scheduled_workers,
|
|
103
|
+
watch: -> { [[".", *@schedule_paths.map { |path| Rigor::RootedGlob.literal(path) }]] } do |_params|
|
|
102
104
|
ScheduleScan.new(
|
|
103
105
|
io_boundary: io_boundary,
|
|
104
106
|
schedule_paths: @schedule_paths
|
|
@@ -62,7 +62,7 @@ module Rigor
|
|
|
62
62
|
if io_boundary.directory?(File.expand_path(entry))
|
|
63
63
|
[entry, "**/*.rb"]
|
|
64
64
|
elsif entry.end_with?(".rb") && io_boundary.file?(File.expand_path(entry))
|
|
65
|
-
[File.dirname(entry), File.basename(entry)]
|
|
65
|
+
[File.dirname(entry), Rigor::RootedGlob.literal(File.basename(entry))]
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -66,8 +66,9 @@ module Rigor
|
|
|
66
66
|
|
|
67
67
|
# Always an Array of Bucket — `initialize` freezes what it is given and every caller hands it
|
|
68
68
|
# Bucket instances (parse_row rows, from_diagnostics groups, `without`'s reject, or `[]`).
|
|
69
|
-
# sig-gen gap: #
|
|
70
|
-
# is skipped (untyped return)
|
|
69
|
+
# sig-gen gap: #2321 — `initialize` assigns `buckets.freeze`, an expression derived from the
|
|
70
|
+
# parameter rather than a bare read of it, so the reader is skipped (untyped return) and the element type
|
|
71
|
+
# is hand-written.
|
|
71
72
|
attr_reader buckets: Array[Bucket]
|
|
72
73
|
|
|
73
74
|
# `project_root` is `Pathname.new`'d, so a `Pathname` is accepted as well as a `String`
|
|
@@ -45,16 +45,15 @@ module Rigor
|
|
|
45
45
|
# The collection a file with nothing to say contributes.
|
|
46
46
|
def self.empty: (?String? path) -> FileCollection
|
|
47
47
|
|
|
48
|
-
# sig-gen gap: #1154 — `initialize` freezes the kwarg tables rather than assigning them
|
|
49
|
-
# verbatim, so sig-gen proposes the default constant's shape for each reader.
|
|
50
48
|
attr_reader path: String?
|
|
51
|
-
# sig-gen gap: #
|
|
49
|
+
# sig-gen gap: #2312 — `{ "Class#m" => Summary }`, through the private `freeze_table` helper, whose parameters
|
|
50
|
+
# are undeclared.
|
|
52
51
|
attr_reader summaries: Hash[String, Summary]
|
|
53
|
-
# sig-gen gap: #
|
|
52
|
+
# sig-gen gap: #2312 — `{ "Class#m" => [Edge] }`, sorted and de-duplicated.
|
|
54
53
|
attr_reader edges: Hash[String, Array[Edge]]
|
|
55
|
-
# sig-gen gap: #
|
|
54
|
+
# sig-gen gap: #2312 — `{ class_name => superclass_name }`, `OPAQUE_ANCESTOR` included.
|
|
56
55
|
attr_reader superclasses: Hash[String, String]
|
|
57
|
-
# sig-gen gap: #
|
|
56
|
+
# sig-gen gap: #2312 — `{ class_name => [module_name] }`, include/prepend merged.
|
|
58
57
|
attr_reader includes: Hash[String, Array[String]]
|
|
59
58
|
|
|
60
59
|
def initialize: (?path: String?, ?summaries: Hash[String, Summary],
|