rigortype 0.4.1 → 0.4.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/README.md +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -31,6 +31,7 @@ require_relative "match_rebinding"
|
|
|
31
31
|
require_relative "element_read_widening"
|
|
32
32
|
require_relative "hash_lookup_mutation"
|
|
33
33
|
require_relative "indexed_narrowing"
|
|
34
|
+
require_relative "key_presence_guard"
|
|
34
35
|
require_relative "index_write_widening"
|
|
35
36
|
require_relative "method_dispatcher"
|
|
36
37
|
require_relative "method_parameter_binder"
|
|
@@ -352,7 +353,7 @@ module Rigor
|
|
|
352
353
|
@on_enter&.call(node, @scope)
|
|
353
354
|
|
|
354
355
|
handler = HANDLERS[node.class]
|
|
355
|
-
return forget_implicit_call_guards(node, send(handler, node)) if handler
|
|
356
|
+
return forget_aliased_key_guards(node, forget_implicit_call_guards(node, send(handler, node))) if handler
|
|
356
357
|
|
|
357
358
|
# Default: the node is treated as a pure expression. Type it through the existing expression typer (which
|
|
358
359
|
# observes the current scope's locals) and leave the scope unchanged, but for the match globals a call in it
|
|
@@ -373,6 +374,16 @@ module Rigor
|
|
|
373
374
|
[type, after.forget_guard_narrowings]
|
|
374
375
|
end
|
|
375
376
|
|
|
377
|
+
# Issue #1703 — a write that stores a `key?` guard's receiver or key root elsewhere (`g = h`) aliases it, so
|
|
378
|
+
# the guard ends ({KeyPresenceGuard.invalidate_after_write}).
|
|
379
|
+
def forget_aliased_key_guards(node, result)
|
|
380
|
+
type, after = result
|
|
381
|
+
return result unless after.is_a?(Scope)
|
|
382
|
+
|
|
383
|
+
guarded = KeyPresenceGuard.invalidate_after_write(node, after)
|
|
384
|
+
guarded.equal?(after) ? result : [type, guarded]
|
|
385
|
+
end
|
|
386
|
+
|
|
376
387
|
# Issue #1359 — the nodes whose handler runs their parts in order, each from the scope the parts before it
|
|
377
388
|
# left, so a `$_` reader among them forgets it where it runs ({LastLine}) and a read before it keeps the
|
|
378
389
|
# narrowing.
|
|
@@ -2994,6 +3005,8 @@ module Rigor
|
|
|
2994
3005
|
# outer receiver is itself a chain node (e.g. `x.last << y`) do NOT drop narrowings keyed on `x` — only direct
|
|
2995
3006
|
# calls against the root variable invalidate the chain.
|
|
2996
3007
|
post_scope = IndexedNarrowing.invalidate_chain_after_call(call_node: node, current_scope: post_scope)
|
|
3008
|
+
# Issue #1703 — a non-literal-key `key?` guard the call may break ({KeyPresenceGuard.breaks?}).
|
|
3009
|
+
post_scope = KeyPresenceGuard.invalidate_after_call(node, post_scope)
|
|
2997
3010
|
# B2.2 — intervening method call ivar invalidation. An implicit-self / self-receiver call could mutate any ivar
|
|
2998
3011
|
# of the enclosing class (we cannot prove purity without an effect system). Reset each ivar whose current local
|
|
2999
3012
|
# binding has narrowed below the class-ivar seed back to the seed itself, so a subsequent `if @flag` predicate
|
|
@@ -3559,15 +3572,19 @@ module Rigor
|
|
|
3559
3572
|
# does not type as a class object (a gem class with no RBS) keeps the name as written: the body is still some
|
|
3560
3573
|
# class's body, and leaving it on the enclosing `self` made a `refine` at the file's top level report every
|
|
3561
3574
|
# implicit-self call in it as `call.unresolved-toplevel`.
|
|
3575
|
+
#
|
|
3576
|
+
# Issue #1689 — a `refine` call the file's {InEffectRefinements} places in a class body is the class's own
|
|
3577
|
+
# method, not `Module#refine`, so its block is entered as any other block.
|
|
3562
3578
|
def refined_class_context(node)
|
|
3563
3579
|
target = ScopeIndexer.refine_target(node)
|
|
3564
3580
|
return nil if target.nil?
|
|
3565
3581
|
|
|
3566
|
-
|
|
3567
|
-
|
|
3582
|
+
return nil if InEffectRefinements.class_body_refine?(scope, node)
|
|
3583
|
+
|
|
3584
|
+
name = ScopeIndexer.refined_class_name(target, scope)
|
|
3568
3585
|
return nil if name.nil?
|
|
3569
3586
|
|
|
3570
|
-
[ClassFrame.new(name: name
|
|
3587
|
+
[ClassFrame.new(name: name, singleton: false, refinement: true)]
|
|
3571
3588
|
end
|
|
3572
3589
|
|
|
3573
3590
|
# The block calls whose body `return` leaves only the block ({ReturnBarrier.block_call?}). Like a `->` body
|
|
@@ -3601,7 +3618,9 @@ module Rigor
|
|
|
3601
3618
|
narrowed = DefineMethodBlockSelf.narrow_self_type_for(
|
|
3602
3619
|
scope: scope, call_node: call_node
|
|
3603
3620
|
)
|
|
3604
|
-
|
|
3621
|
+
return block_entry unless narrowed
|
|
3622
|
+
|
|
3623
|
+
block_entry.with_block_self_type(narrowed, keeps_unknown: scope.block_self_narrowing_unknown?(call_node))
|
|
3605
3624
|
end
|
|
3606
3625
|
|
|
3607
3626
|
# Enters a meta-new `block` as the body of the class `class_context` names: `self_type` is that class's
|
|
@@ -3616,8 +3635,17 @@ module Rigor
|
|
|
3616
3635
|
# record a `Outer::K` entry: that is a rung Ruby's constant lookup does not have. The `class_context`
|
|
3617
3636
|
# frame is still pushed — it is what a nested `def` registers its method under — which is exactly the
|
|
3618
3637
|
# divergence that makes the chain a separate record rather than a view of the frame stack.
|
|
3638
|
+
#
|
|
3639
|
+
# Issue #1759 — a `refine X do … end` block runs on the refinement module, not on X, which Rigor does not
|
|
3640
|
+
# model: the body keeps X's frame and singleton so its `def`s and implicit-self calls resolve as before, but
|
|
3641
|
+
# carries the unknown-`self` mark ({Scope#block_self_unknown?}), so an explicit `self.import_methods(M)` is
|
|
3642
|
+
# not checked against `singleton(X)`. The indexer leaves a value-position refine block unknown the same way.
|
|
3619
3643
|
def enter_meta_class_body(block, block_entry, class_context)
|
|
3620
|
-
|
|
3644
|
+
# Issue #1717 — the body runs on the new class whatever encloses it, so its `self` is known.
|
|
3645
|
+
entry = block_entry.with_block_self_type(self_type_for_class_body(class_context),
|
|
3646
|
+
keeps_unknown: class_context.last.refinement &&
|
|
3647
|
+
Scope::REFINEMENT_BLOCK_SELF)
|
|
3648
|
+
.with_singleton_class_body(false)
|
|
3621
3649
|
sub_eval(block, stamp_nesting(entry, @lexical_nesting), class_context: class_context)
|
|
3622
3650
|
end
|
|
3623
3651
|
|
|
@@ -5140,27 +5168,31 @@ module Rigor
|
|
|
5140
5168
|
# ({ExpressionTyper#block_body_self_narrowing}) already applies the same contract to block-return
|
|
5141
5169
|
# typing; without it here the recorded per-node scopes — what `dump_type`/`assert_type` and the
|
|
5142
5170
|
# survey read — keep the enclosing `self_type` and every DSL call inside stays `Dynamic[top]`.
|
|
5143
|
-
|
|
5144
|
-
|
|
5171
|
+
# Issue #1667 — the entry's `refinements:` are put in effect in the body alongside.
|
|
5172
|
+
match = call_node && macro_block_match(call_node)
|
|
5173
|
+
if match
|
|
5174
|
+
keeps_unknown = scope.block_self_narrowing_unknown?(call_node)
|
|
5175
|
+
scope_with_params = MacroBlockSelfType.apply(scope_with_params, match, keeps_unknown: keeps_unknown)
|
|
5176
|
+
end
|
|
5145
5177
|
block_local_names(block_node).reduce(scope_with_params) do |acc, name|
|
|
5146
5178
|
acc.with_local(name, Type::Combinator.constant_of(nil))
|
|
5147
5179
|
end
|
|
5148
5180
|
end
|
|
5149
5181
|
|
|
5150
|
-
# The
|
|
5182
|
+
# The ADR-16 `block_as_methods:` match for `call_node`, keyed on the explicit receiver's type, or on the
|
|
5151
5183
|
# current `self_type` for an implicit-self DSL call (the `params do` / `namespace do` shapes, whose
|
|
5152
5184
|
# receiver is the enclosing `Singleton[X]`). A miss leaves the entry scope as built — the false-
|
|
5153
5185
|
# positive-safe direction.
|
|
5154
|
-
def
|
|
5186
|
+
def macro_block_match(call_node)
|
|
5155
5187
|
receiver_type =
|
|
5156
5188
|
if call_node.receiver
|
|
5157
5189
|
explicit_receiver_type(call_node)
|
|
5158
5190
|
else
|
|
5159
|
-
scope
|
|
5191
|
+
MacroBlockSelfType.implicit_receiver_type(scope)
|
|
5160
5192
|
end
|
|
5161
5193
|
return nil if receiver_type.nil?
|
|
5162
5194
|
|
|
5163
|
-
MacroBlockSelfType.
|
|
5195
|
+
MacroBlockSelfType.match_for(
|
|
5164
5196
|
scope: scope, call_node: call_node, receiver_type: receiver_type
|
|
5165
5197
|
)
|
|
5166
5198
|
rescue StandardError
|
|
@@ -5193,7 +5225,8 @@ module Rigor
|
|
|
5193
5225
|
method_name: call_node.name,
|
|
5194
5226
|
arg_types: arg_types,
|
|
5195
5227
|
environment: scope.environment,
|
|
5196
|
-
scope: scope
|
|
5228
|
+
scope: scope,
|
|
5229
|
+
call_node: call_node
|
|
5197
5230
|
)
|
|
5198
5231
|
rescue StandardError
|
|
5199
5232
|
[]
|
|
@@ -5218,13 +5251,19 @@ module Rigor
|
|
|
5218
5251
|
fresh = build_fresh_body_scope
|
|
5219
5252
|
# Issue #1518 — `class << self` inside an unnameable class body keeps that body's opacity: it opens the
|
|
5220
5253
|
# singleton of a class nothing names, and its empty frame stack is not the top level.
|
|
5221
|
-
|
|
5222
|
-
|
|
5254
|
+
singleton_body = node.is_a?(Prism::SingletonClassNode)
|
|
5255
|
+
opaque_self ||= singleton_body && @opaque_class && new_context.empty?
|
|
5256
|
+
# Issue #1763 — a `class << self` in a `refine X` block (the refinement frame {#singleton_context_for} built)
|
|
5257
|
+
# opens the refinement module's singleton class, which Rigor does not model, as the `def`s in it already read
|
|
5258
|
+
# ({#self_type_for_method_body}); `Singleton[X]` there checked `self.m` and a local holding `self` against X's
|
|
5259
|
+
# class methods. A `class << X` names X's real singleton class and takes a frame without the flag.
|
|
5260
|
+
untyped_self = opaque_self || (singleton_body && new_context.last&.refinement)
|
|
5261
|
+
body_self = untyped_self ? Type::Combinator.untyped : self_type_for_class_body(new_context)
|
|
5223
5262
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
5224
5263
|
# Issue #963 — `self` in a `class << ...` body is the SINGLETON class, which shares the `Singleton[X]`
|
|
5225
5264
|
# carrier a `class X` body gets. The mark is the only thing that tells the two apart downstream, and a
|
|
5226
5265
|
# `def` reached from this body clears it by starting from a fresh scope.
|
|
5227
|
-
fresh = fresh.with_singleton_class_body(
|
|
5266
|
+
fresh = fresh.with_singleton_class_body(singleton_body)
|
|
5228
5267
|
fresh = fresh.with_match_frame(node.body)
|
|
5229
5268
|
fresh = stamp_nesting(fresh, new_nesting)
|
|
5230
5269
|
sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting, opaque_class: opaque_self)
|
|
@@ -5254,7 +5293,7 @@ module Rigor
|
|
|
5254
5293
|
# then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
|
|
5255
5294
|
# class-level accumulator so `def get; @x; end` reads the type that a sibling `def init; @x = 1; end` wrote.
|
|
5256
5295
|
fresh = build_fresh_body_scope
|
|
5257
|
-
body_self = self_type_for_method_body(singleton: singleton)
|
|
5296
|
+
body_self = self_type_for_method_body(singleton: singleton, receiver: def_node.receiver)
|
|
5258
5297
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
5259
5298
|
# A `def` opens no new `Module.nesting` entry — the body resolves constants against the chain of the
|
|
5260
5299
|
# declaration that encloses it, which is what the evaluator is already carrying (#652).
|
|
@@ -5389,12 +5428,16 @@ module Rigor
|
|
|
5389
5428
|
# chain's throwaway intermediate Scopes were a top `Scope#rebuild` source (ADR-44). Local-empty by design; the
|
|
5390
5429
|
# discovery index is inherited whole by reference (ADR-53 Track A), so a table added to the index can no longer
|
|
5391
5430
|
# be dropped here by a missed per-field copy.
|
|
5431
|
+
#
|
|
5432
|
+
# Issue #1667 — a plugin-declared refined block's modules carry into a `def` or class body written in it: CRuby
|
|
5433
|
+
# defines the method with the block's cref, and a class body's cref inherits the outer one's refinements.
|
|
5392
5434
|
Scope.new(
|
|
5393
5435
|
environment: scope.environment,
|
|
5394
5436
|
locals: {}.freeze,
|
|
5395
5437
|
source_path: scope.source_path,
|
|
5396
5438
|
discovery: scope.discovery,
|
|
5397
|
-
dynamic_origins: scope.dynamic_origins
|
|
5439
|
+
dynamic_origins: scope.dynamic_origins,
|
|
5440
|
+
declared_refinements: scope.declared_refinements
|
|
5398
5441
|
)
|
|
5399
5442
|
end
|
|
5400
5443
|
|
|
@@ -5444,11 +5487,15 @@ module Rigor
|
|
|
5444
5487
|
# `Nominal[Foo]`.
|
|
5445
5488
|
#
|
|
5446
5489
|
# Returns nil for top-level defs that have no enclosing class.
|
|
5447
|
-
def self_type_for_method_body(singleton:)
|
|
5490
|
+
def self_type_for_method_body(singleton:, receiver: nil)
|
|
5448
5491
|
path = current_class_path
|
|
5449
5492
|
# Issue #1518 — a `def` in a class nothing names has a receiver, just not one Rigor can type; nil would
|
|
5450
5493
|
# read its body as the top level and its implicit-self calls as unresolved top-level calls.
|
|
5451
5494
|
return (Type::Combinator.untyped if @opaque_class) if path.nil?
|
|
5495
|
+
# Issue #1759 — a `def self.m` in a `refine X` block (or a `def m` in a `class << self` there) is a method
|
|
5496
|
+
# of the refinement module, which Rigor does not model; reading it as `Singleton[X]` checked its
|
|
5497
|
+
# explicit-self calls against X's class methods. A `def X.m` there does define X's class method.
|
|
5498
|
+
return Type::Combinator.untyped if singleton && @class_context.last.refinement && refinement_self_def?(receiver)
|
|
5452
5499
|
|
|
5453
5500
|
if singleton
|
|
5454
5501
|
Type::Combinator.singleton_of(path)
|
|
@@ -5457,6 +5504,10 @@ module Rigor
|
|
|
5457
5504
|
end
|
|
5458
5505
|
end
|
|
5459
5506
|
|
|
5507
|
+
# Issue #1759 — whether a singleton `def` under a refinement frame defines a method on the refinement module
|
|
5508
|
+
# (`def self.m`, or `def m` in a `class << self`) rather than on an explicitly named receiver (`def X.m`).
|
|
5509
|
+
def refinement_self_def?(receiver) = receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
5510
|
+
|
|
5460
5511
|
def singleton_context_for(node)
|
|
5461
5512
|
case node.expression
|
|
5462
5513
|
when Prism::SelfNode
|
|
@@ -5464,7 +5515,7 @@ module Rigor
|
|
|
5464
5515
|
|
|
5465
5516
|
outer = @class_context[0..-2]
|
|
5466
5517
|
last = @class_context.last
|
|
5467
|
-
outer + [ClassFrame.new(name: last.name, singleton: true)]
|
|
5518
|
+
outer + [ClassFrame.new(name: last.name, singleton: true, refinement: last.refinement)]
|
|
5468
5519
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
5469
5520
|
target = singleton_constant_target(node.expression)
|
|
5470
5521
|
return @class_context unless target
|
|
@@ -22,6 +22,9 @@ module Rigor
|
|
|
22
22
|
# the effect classifier and the effect catalogue's `mutators: string` cite it rather than keep a list of their
|
|
23
23
|
# own (ADR-103 WD3). A spec holds it to every bang method `String` defines plus the non-bang mutators reflection
|
|
24
24
|
# cannot find by name.
|
|
25
|
+
#
|
|
26
|
+
# The last row is Ruby 4.1's bit operations (#1691), which `data/core_overlay/string.rbs` declares whatever Ruby
|
|
27
|
+
# runs the analysis; they rewrite bytes in place and keep the byte length.
|
|
25
28
|
MUTATORS = %i[
|
|
26
29
|
<< concat insert prepend replace clear []= slice!
|
|
27
30
|
setbyte bytesplice append_as_bytes force_encoding
|
|
@@ -29,18 +32,21 @@ module Rigor
|
|
|
29
32
|
upcase! downcase! capitalize! swapcase! reverse!
|
|
30
33
|
strip! lstrip! rstrip! chomp! chop!
|
|
31
34
|
delete_prefix! delete_suffix! encode! scrub! unicode_normalize!
|
|
35
|
+
bit_set bit_clear bit_flip bitwise_not! bitwise_and! bitwise_or! bitwise_xor!
|
|
32
36
|
].to_set.freeze
|
|
33
37
|
|
|
34
38
|
# The {MUTATORS} that CANNOT leave a non-empty receiver empty, and so keep a `non-empty-string` refinement:
|
|
35
39
|
# appenders (`<<`, `concat`, `insert`, `prepend`, `append_as_bytes`), and rewrites that keep at least one
|
|
36
40
|
# character (`setbyte`, `force_encoding`, the case mappings, `reverse!`, `succ!` / `next!`, `squeeze!`,
|
|
37
|
-
# `unicode_normalize
|
|
38
|
-
#
|
|
39
|
-
#
|
|
41
|
+
# `unicode_normalize!`, and the bit operations, which keep every byte). A whitelist, as
|
|
42
|
+
# `RefinementMutation::EMPTY_PRESERVING` is for Array and Hash: a mutator missing here retracts the witness, which
|
|
43
|
+
# costs precision, where a mutator missing from a list of emptiers kept the witness of a string it had just
|
|
44
|
+
# emptied — `tr!` and `tr_s!` did, since `"a".tr!("a", "")` is `""`.
|
|
40
45
|
EMPTY_PRESERVING = %i[
|
|
41
46
|
<< concat insert prepend append_as_bytes
|
|
42
47
|
setbyte force_encoding
|
|
43
48
|
upcase! downcase! capitalize! swapcase! reverse! succ! next! squeeze! unicode_normalize!
|
|
49
|
+
bit_set bit_clear bit_flip bitwise_not! bitwise_and! bitwise_or! bitwise_xor!
|
|
44
50
|
].to_set.freeze
|
|
45
51
|
|
|
46
52
|
# `tr!` / `tr_s!` map every character they match to one character of the replacement (padded with its last), so
|
|
@@ -333,7 +333,8 @@ module Rigor
|
|
|
333
333
|
|
|
334
334
|
def collect_included_do_calls(body)
|
|
335
335
|
body.body.flat_map do |stmt|
|
|
336
|
-
|
|
336
|
+
included_do = stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :included
|
|
337
|
+
next [] unless included_do && stmt.block.is_a?(Prism::BlockNode)
|
|
337
338
|
|
|
338
339
|
block_body = stmt.block.body
|
|
339
340
|
next [] unless block_body.respond_to?(:body)
|
|
@@ -33,6 +33,10 @@ module Rigor
|
|
|
33
33
|
# Prism *parse* version (it decides which syntax is accepted), it is not threaded to the inference
|
|
34
34
|
# layer, and its default (`"4.0"`) is a default rather than a user statement about the runtime.
|
|
35
35
|
#
|
|
36
|
+
# The one exception is the Ruby-deprecation rule (the ADR-47 WD5 amendment, #1692). An explicit
|
|
37
|
+
# `target_ruby` is a user statement about the runtime, and that rule passes it as `stated_ruby:` to ask
|
|
38
|
+
# whether a deprecated call can run on the stated Ruby. Narrowing and the dead-arm filter never pass it.
|
|
39
|
+
#
|
|
36
40
|
# ## What is folded (and what is deliberately not)
|
|
37
41
|
#
|
|
38
42
|
# * `RUBY_VERSION <cmp> "x.y.z"` — compared with **String** semantics, because that is what runs.
|
|
@@ -91,9 +95,13 @@ module Rigor
|
|
|
91
95
|
private_constant :GEM_VERSION_PATH
|
|
92
96
|
|
|
93
97
|
# @param node — an `if` / `unless` predicate
|
|
94
|
-
# @
|
|
98
|
+
# @param stated_ruby — ADR-47 WD5 amendment (#1692): a `x.y.z` version to read `RUBY_VERSION` as instead of
|
|
99
|
+
# the analyzer's own, for the Ruby-deprecation rules only ({Configuration#stated_runtime_ruby}). Under it
|
|
100
|
+
# nothing else is readable — `RUBY_ENGINE` and the default gems' `VERSION`s belong to the analyzer's
|
|
101
|
+
# interpreter, not to the stated one — so a guard on them answers nil.
|
|
102
|
+
# @return `:truthy` / `:falsey` when the guard is decidable on the analyzer's Ruby (or on `stated_ruby`),
|
|
95
103
|
# otherwise nil (both arms stay live)
|
|
96
|
-
def verdict(node)
|
|
104
|
+
def verdict(node, stated_ruby: nil)
|
|
97
105
|
return nil unless node.is_a?(Prism::CallNode)
|
|
98
106
|
|
|
99
107
|
operator = node.name
|
|
@@ -103,19 +111,19 @@ module Rigor
|
|
|
103
111
|
arguments = node.arguments&.arguments
|
|
104
112
|
return nil unless arguments && arguments.length == 1
|
|
105
113
|
|
|
106
|
-
left = read_operand(node.receiver)
|
|
107
|
-
right = read_operand(arguments.first)
|
|
114
|
+
left = read_operand(node.receiver, stated_ruby)
|
|
115
|
+
right = read_operand(arguments.first, stated_ruby)
|
|
108
116
|
return nil if left.nil? || right.nil?
|
|
109
117
|
|
|
110
118
|
decide(left, right, operator)
|
|
111
119
|
end
|
|
112
120
|
|
|
113
121
|
# Reads one side of the comparison into `[kind, value]`, or nil when it is not readable.
|
|
114
|
-
def read_operand(node)
|
|
122
|
+
def read_operand(node, stated_ruby = nil)
|
|
115
123
|
case node
|
|
116
124
|
when Prism::StringNode then [:literal_string, node.unescaped]
|
|
117
|
-
when Prism::ConstantReadNode, Prism::ConstantPathNode then read_constant(node)
|
|
118
|
-
when Prism::CallNode then read_gem_version(node)
|
|
125
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then read_constant(node, stated_ruby)
|
|
126
|
+
when Prism::CallNode then read_gem_version(node, stated_ruby)
|
|
119
127
|
end
|
|
120
128
|
end
|
|
121
129
|
private_class_method :read_operand
|
|
@@ -129,9 +137,10 @@ module Rigor
|
|
|
129
137
|
#
|
|
130
138
|
# The foldable set is unchanged and stays closed: an unqualified name folds only when it is one of
|
|
131
139
|
# {PREDEFINED}, a qualified one only when it is in {VERSION_CONSTANTS}.
|
|
132
|
-
def read_constant(node)
|
|
140
|
+
def read_constant(node, stated_ruby = nil)
|
|
133
141
|
path = Source::ConstantPath.qualified_name_or_nil(node)
|
|
134
142
|
return nil unless path
|
|
143
|
+
return (path == "RUBY_VERSION" ? [:string, stated_ruby] : nil) if stated_ruby
|
|
135
144
|
|
|
136
145
|
kind =
|
|
137
146
|
if path.include?("::")
|
|
@@ -149,14 +158,14 @@ module Rigor
|
|
|
149
158
|
# `Gem::Version.new(<readable>)`. The inner operand must be a plain version String — an engine name
|
|
150
159
|
# is not a version, and `Gem::Version.new` raises on anything `Gem::Version.correct?` rejects, so a
|
|
151
160
|
# malformed literal keeps both arms live instead of folding a guard the program cannot even reach.
|
|
152
|
-
def read_gem_version(node)
|
|
161
|
+
def read_gem_version(node, stated_ruby = nil)
|
|
153
162
|
return nil unless node.name == :new && node.block.nil?
|
|
154
163
|
return nil unless Source::ConstantPath.qualified_name_or_nil(node.receiver) == GEM_VERSION_PATH
|
|
155
164
|
|
|
156
165
|
arguments = node.arguments&.arguments
|
|
157
166
|
return nil unless arguments && arguments.length == 1
|
|
158
167
|
|
|
159
|
-
inner = read_operand(arguments.first)
|
|
168
|
+
inner = read_operand(arguments.first, stated_ruby)
|
|
160
169
|
return nil unless inner && STRING_KINDS.include?(inner.first)
|
|
161
170
|
return nil unless ::Gem::Version.correct?(inner.last)
|
|
162
171
|
|
|
@@ -46,6 +46,9 @@ module Rigor
|
|
|
46
46
|
|
|
47
47
|
def initialize(configuration:)
|
|
48
48
|
@configuration = configuration
|
|
49
|
+
@buffer_sources = {}
|
|
50
|
+
@disk_feature_files = nil
|
|
51
|
+
@environment_features = nil
|
|
49
52
|
@generation = 0
|
|
50
53
|
@environment = nil
|
|
51
54
|
@cache_store = nil
|
|
@@ -77,6 +80,9 @@ module Rigor
|
|
|
77
80
|
dependency_source_index: project_scan.dependency_source_index,
|
|
78
81
|
synthetic_method_index: project_scan.synthetic_method_index,
|
|
79
82
|
project_patched_methods: project_scan.project_patched_methods,
|
|
83
|
+
# Issue #1700 — the configured paths' files overlaid with the open buffers; {#note_buffers}
|
|
84
|
+
# rebuilds this when an edit or a save changes the set.
|
|
85
|
+
required_features: (@environment_features = required_features),
|
|
80
86
|
**ProjectEnvironment.dependency_discovery_options(@configuration)
|
|
81
87
|
)
|
|
82
88
|
end
|
|
@@ -119,12 +125,37 @@ module Rigor
|
|
|
119
125
|
diagnostics
|
|
120
126
|
end
|
|
121
127
|
|
|
128
|
+
# Issue #1700 — records the open buffers' text (absolute path => source) and rebuilds the environment
|
|
129
|
+
# when it changes the {Environment::RequiredFeatures} set the current one was built with: a buffer that
|
|
130
|
+
# gains or drops a `require "prime"` loads or unloads the vendored `prime` signatures. The server calls
|
|
131
|
+
# it on every open, change, save and close; neither `didSave` nor a `.rb` edit reaches
|
|
132
|
+
# `workspace/didChangeWatchedFiles`, which most clients send only for the files they were asked to watch.
|
|
133
|
+
# The configured paths are read once per environment and only the buffers are re-scanned, so a keystroke
|
|
134
|
+
# costs a substring test per open buffer.
|
|
135
|
+
def note_buffers(sources)
|
|
136
|
+
@buffer_sources = sources
|
|
137
|
+
return if @environment.nil? || required_features == @environment_features
|
|
138
|
+
|
|
139
|
+
invalidate!
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The required-feature set for the configured paths' files on disk, each open buffer read in place of its
|
|
143
|
+
# file.
|
|
144
|
+
def required_features
|
|
145
|
+
disk = (@disk_feature_files ||= Environment::RequiredFeatures.feature_files(@configuration))
|
|
146
|
+
found = disk.reject { |path, _| @buffer_sources.key?(path) }.values.flatten
|
|
147
|
+
found += @buffer_sources.values.flat_map { |source| Environment::RequiredFeatures.features_of(source) }
|
|
148
|
+
found.uniq.sort.freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
122
151
|
# Drops every cached collaborator and bumps the generation. The next reader rebuilds from scratch.
|
|
123
152
|
# Triggered by `workspace/didChangeWatchedFiles` for project source files and by
|
|
124
153
|
# `workspace/didChangeConfiguration`.
|
|
125
154
|
def invalidate!
|
|
126
155
|
@generation += 1
|
|
127
156
|
@environment = nil
|
|
157
|
+
@environment_features = nil
|
|
158
|
+
@disk_feature_files = nil
|
|
128
159
|
@project_scan = nil
|
|
129
160
|
# The session's per-file cache was computed against the old environment / project scan, so it cannot
|
|
130
161
|
# outlive them. The next save round primes a fresh one.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../version"
|
|
4
4
|
require_relative "buffer_table"
|
|
5
|
+
require_relative "uri"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module LanguageServer
|
|
@@ -221,6 +222,7 @@ module Rigor
|
|
|
221
222
|
bytes: doc.fetch(:text),
|
|
222
223
|
version: doc.fetch(:version)
|
|
223
224
|
)
|
|
225
|
+
note_buffers
|
|
224
226
|
@publisher&.publish_for(uri)
|
|
225
227
|
nil
|
|
226
228
|
end
|
|
@@ -242,6 +244,7 @@ module Rigor
|
|
|
242
244
|
changes: changes,
|
|
243
245
|
version: doc.fetch(:version)
|
|
244
246
|
)
|
|
247
|
+
note_buffers
|
|
245
248
|
@publisher&.publish_for(uri)
|
|
246
249
|
nil
|
|
247
250
|
end
|
|
@@ -257,6 +260,7 @@ module Rigor
|
|
|
257
260
|
def handle_did_save(params)
|
|
258
261
|
uri = params.fetch(:textDocument).fetch(:uri)
|
|
259
262
|
@buffer_table.save(uri: uri)
|
|
263
|
+
note_buffers
|
|
260
264
|
@publisher&.publish_project(uri)
|
|
261
265
|
nil
|
|
262
266
|
end
|
|
@@ -361,10 +365,24 @@ module Rigor
|
|
|
361
365
|
doc = params.fetch(:textDocument)
|
|
362
366
|
uri = doc.fetch(:uri)
|
|
363
367
|
@buffer_table.close(uri: uri)
|
|
368
|
+
note_buffers
|
|
364
369
|
@publisher&.publish_empty(uri)
|
|
365
370
|
nil
|
|
366
371
|
end
|
|
367
372
|
|
|
373
|
+
# Issue #1700 — hands the open Ruby buffers to the project context, which rebuilds its environment when
|
|
374
|
+
# they change the set of required features the vendored signatures are gated on.
|
|
375
|
+
def note_buffers
|
|
376
|
+
return unless @project_context.respond_to?(:note_buffers)
|
|
377
|
+
|
|
378
|
+
sources = @buffer_table.uris.each_with_object({}) do |uri, acc|
|
|
379
|
+
path = Uri.to_path(uri)
|
|
380
|
+
entry = @buffer_table[uri]
|
|
381
|
+
acc[File.expand_path(path)] = entry.bytes if path&.end_with?(".rb") && entry
|
|
382
|
+
end
|
|
383
|
+
@project_context.note_buffers(sources)
|
|
384
|
+
end
|
|
385
|
+
|
|
368
386
|
def method_not_found(method)
|
|
369
387
|
rpc_error(ERROR_METHOD_NOT_FOUND, "method not found: #{method.inspect}")
|
|
370
388
|
end
|
|
@@ -54,7 +54,7 @@ module Rigor
|
|
|
54
54
|
# by `rigor plugins` (ADR-90) to probe {available?} at activation time — so a standalone install
|
|
55
55
|
# where inflection silently degrades reports the degradation instead of an unqualified `[OK]`. When
|
|
56
56
|
# a new plugin adopts the helper, add its manifest id here.
|
|
57
|
-
CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord factorybot rails-routes].freeze
|
|
57
|
+
CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord alba factorybot rails-routes].freeze
|
|
58
58
|
|
|
59
59
|
# Raised when `ActiveSupport::Inflector` is required for an inflection but cannot be loaded. Caught by
|
|
60
60
|
# the per-plugin isolation boundary, so it surfaces as "this plugin produced no diagnostics" rather than
|
|
@@ -199,6 +199,44 @@ module Rigor
|
|
|
199
199
|
entries
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
+
# Issue #1652 — the MEMBERSHIP fingerprint of a `Dir.glob` pattern under `root`, `**` included. Returns the
|
|
203
|
+
# matching paths and records one `:names` {Cache::Descriptor::GlobEntry} over `root` and `pattern`: the row
|
|
204
|
+
# reads stale when a match appears or disappears, in a subdirectory created after the read too, and NOT
|
|
205
|
+
# when a match's bytes or stat tuple move. A caller whose answer depends on a match's content MUST
|
|
206
|
+
# {#read_file} each match it reads; that row carries the edit. A stat signature here would read stale on
|
|
207
|
+
# every `touch`, checkout or restored CI cache, though the content the caller read is unchanged (#979's
|
|
208
|
+
# reasoning). {#list_directory} keeps its `:stat` listing row.
|
|
209
|
+
#
|
|
210
|
+
# The pattern must stay under `root`, since the policy is checked against `root` alone: a `..` segment, a
|
|
211
|
+
# brace (`{..,x}`) or a backslash escape (`.\\./`) raises {AccessDeniedError} (`:read_outside_scope`).
|
|
212
|
+
# The last two are refused outright because `Dir.glob` can spell `..` with them. Out of policy, `root` is
|
|
213
|
+
# globbed truthfully and contributes no row, as in {#list_directory}.
|
|
214
|
+
#
|
|
215
|
+
# @param root — project directory; relative paths expand against the working directory
|
|
216
|
+
# @param pattern — a `Dir.glob` pattern relative to `root`, such as `"ext/**/extconf.rb"`
|
|
217
|
+
# @return the absolute matching paths in `Dir.glob` order
|
|
218
|
+
def glob(root, pattern)
|
|
219
|
+
absolute = File.absolute_path(root.to_s)
|
|
220
|
+
pattern = pattern.to_s
|
|
221
|
+
if pattern.match?(/[{\\]/) || pattern.split("/").include?("..")
|
|
222
|
+
raise AccessDeniedError.new(
|
|
223
|
+
"plugin #{@plugin_id.inspect} cannot glob #{pattern.inspect} under #{absolute.inspect}: " \
|
|
224
|
+
"a `..` segment, a brace or a backslash can leave the root the trusted-read scope was checked against",
|
|
225
|
+
reason: :read_outside_scope,
|
|
226
|
+
resource: File.join(absolute, pattern)
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
entries = Dir.glob(File.join(absolute, pattern))
|
|
231
|
+
unless @policy.allow_read?(absolute)
|
|
232
|
+
record_refusal(absolute)
|
|
233
|
+
return entries
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
record_glob_entry(absolute, pattern, mode: :names)
|
|
237
|
+
entries
|
|
238
|
+
end
|
|
239
|
+
|
|
202
240
|
# Fetches the URL when the policy permits it. Returns the response body. Raises {AccessDeniedError} when
|
|
203
241
|
# the policy is `:disabled`, the URL scheme is not `https`, the host is not on the allowlist, the
|
|
204
242
|
# response is non-2xx, the body exceeds {URL_MAX_BYTES}, or the request times out
|
|
@@ -352,11 +390,12 @@ module Rigor
|
|
|
352
390
|
self.class.attribute(entry)
|
|
353
391
|
end
|
|
354
392
|
|
|
355
|
-
# ADR-45 WD1c (#629) — the listing row for {#list_directory}
|
|
393
|
+
# ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob}),
|
|
394
|
+
# deduplicated per (root, pattern, mode) slot.
|
|
356
395
|
# Recomputed on each call rather than `||=`d, so a directory listed again after a mid-run mutation
|
|
357
396
|
# carries the signature of the state the LAST reader saw.
|
|
358
|
-
def record_glob_entry(root, pattern)
|
|
359
|
-
entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern)
|
|
397
|
+
def record_glob_entry(root, pattern, mode: :stat)
|
|
398
|
+
entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern, mode: mode)
|
|
360
399
|
@mutex.synchronize { @glob_entries[entry.slot_key] = entry }
|
|
361
400
|
self.class.attribute(entry)
|
|
362
401
|
end
|