rigortype 0.4.1 → 0.4.3
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 +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- 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 +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- 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/effects_cache_probe.rb +1 -1
- 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 +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- 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 +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- 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 +286 -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/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -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/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- 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_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -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 +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- 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 +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- 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-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -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 +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -1
|
@@ -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)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
require_relative "../source/node_walker"
|
|
5
|
+
require_relative "captured_locals"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Issue #1840 — a branch merge that re-joins the narrowed edges of an untyped local gives it back its
|
|
10
|
+
# untyped binding.
|
|
11
|
+
#
|
|
12
|
+
# Flow narrowing concretizes a `Dynamic[top]` receiver (ADR-83): `x.nil?` splits it into `nil` and
|
|
13
|
+
# `Dynamic[top]`, and `when String` into `String` and `Dynamic[top]`. `Scope#join` unions the edges, so
|
|
14
|
+
# after both merges the local reads `Dynamic[top] | String | nil`. That union is not wrong as a set, but
|
|
15
|
+
# every member is a narrowing of the one untyped value, so it says nothing the pre-branch binding did
|
|
16
|
+
# not. It does, however, hold `nil` beside a nameable class, and `call.possible-nil-receiver` reports
|
|
17
|
+
# `x + "a"` on correct code although neither narrowing alone did.
|
|
18
|
+
#
|
|
19
|
+
# The rule: at a merge, a local whose pre-branch binding is exactly `Dynamic[top]`, that no merged arm
|
|
20
|
+
# writes, and whose joined binding is a union still carrying a `Dynamic` member, is bound to
|
|
21
|
+
# `Dynamic[top]` again.
|
|
22
|
+
#
|
|
23
|
+
# - The write test keeps a nil the program puts there: `x = nil if c` writes `x`, so the join stays
|
|
24
|
+
# `Dynamic[top] | nil` and still reports. It is syntactic, over every local-write form under the arms
|
|
25
|
+
# (a pattern capture and a block's write to the outer local included), so a doubtful arm keeps today's
|
|
26
|
+
# join. It does not see a written nil that a `nil?` edge stripped before the merge (`x = nil if c; if
|
|
27
|
+
# x.nil? … elsif x.is_a?(String) … end`): that merge's entry is exactly `Dynamic[top]`, and the outer
|
|
28
|
+
# join's `Dynamic[top] | nil` is one the possible-nil gate declines anyway (#1876).
|
|
29
|
+
# - The `Dynamic`-member test keeps a narrowing whose surviving arms no longer cover the untyped value:
|
|
30
|
+
# `case x when String then … when Integer then … else raise end` leaves `Integer | String`, which is
|
|
31
|
+
# what `x` is past it.
|
|
32
|
+
# - Only the `Dynamic[top]` facet: a faceted `Dynamic[T]` carries static knowledge of its own (a
|
|
33
|
+
# plugin's or a signature's `T`), so a `nil` its narrowing leaves may be one the facet promised. Those
|
|
34
|
+
# keep today's join.
|
|
35
|
+
#
|
|
36
|
+
# Restoring `Dynamic[top]` is always sound, since every narrowed edge is a subset of it; what it drops
|
|
37
|
+
# is only the nameable arms, which an untyped union never needed to dispatch.
|
|
38
|
+
module UntypedRejoin
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# `joined` with each qualifying local rebound to its `entry` binding. `arms` are the nodes whose
|
|
42
|
+
# evaluation produced the merged scopes (`nil` entries are skipped).
|
|
43
|
+
def apply(joined, entry, arms)
|
|
44
|
+
candidates = nil
|
|
45
|
+
joined.locals.each do |name, type|
|
|
46
|
+
next unless candidate?(entry.locals[name], type)
|
|
47
|
+
|
|
48
|
+
(candidates ||= []) << name
|
|
49
|
+
end
|
|
50
|
+
return joined if candidates.nil?
|
|
51
|
+
|
|
52
|
+
written = written_locals(arms)
|
|
53
|
+
kept = candidates.reject { |name| written.include?(name) }
|
|
54
|
+
return joined if kept.empty?
|
|
55
|
+
|
|
56
|
+
joined.with_rejoined_locals(kept.to_h { |name| [name, entry.locals[name]] })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def candidate?(entry_type, joined_type)
|
|
60
|
+
entry_type.is_a?(Type::Dynamic) && entry_type.static_facet.is_a?(Type::Top) &&
|
|
61
|
+
joined_type.is_a?(Type::Union) && joined_type.members.any?(Type::Dynamic)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def written_locals(arms)
|
|
65
|
+
names = Set.new
|
|
66
|
+
arms.each do |arm|
|
|
67
|
+
Source::NodeWalker.each(arm) do |node|
|
|
68
|
+
names << node.name if CapturedLocals::LOCAL_WRITE_NODES.include?(node.class)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
names
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -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
|
|
|
@@ -6,6 +6,7 @@ require_relative "../cache/store"
|
|
|
6
6
|
require_relative "../analysis/runner"
|
|
7
7
|
require_relative "../analysis/incremental_session"
|
|
8
8
|
require_relative "../cache/incremental_snapshot"
|
|
9
|
+
require_relative "../effects/identity"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module LanguageServer
|
|
@@ -24,6 +25,10 @@ module Rigor
|
|
|
24
25
|
# are small (the per-publish path is already ≈2ms once Environment is warm).
|
|
25
26
|
#
|
|
26
27
|
# Invalidation:
|
|
28
|
+
# - Every whole-project round (`#project_diagnostics`) re-reads the configuration and compares the
|
|
29
|
+
# incremental snapshot fingerprint and the `effects:` identity with the ones its session was primed with,
|
|
30
|
+
# re-baselining on any change — so no file watcher or `didChangeConfiguration` is needed for
|
|
31
|
+
# `.rigor.yml`, `sig/`, `pre_eval:` or lockfile edits (#1613).
|
|
27
32
|
# - `#invalidate!` drops the cached environment AND project scan + bumps the generation counter; the next
|
|
28
33
|
# reader rebuilds. Watched-file changes (`workspace/didChangeWatchedFiles`) and configuration refreshes
|
|
29
34
|
# (`workspace/didChangeConfiguration`) both trigger this — the next publish observes the new project
|
|
@@ -44,8 +49,16 @@ module Rigor
|
|
|
44
49
|
# invalidated never reach the editor.
|
|
45
50
|
attr_reader :configuration, :generation
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
# `configuration_loader` re-reads the configuration (e.g. `-> { Configuration.load(path) }`); a save round
|
|
53
|
+
# calls it so an edit to `.rigor.yml` (or a file it `includes:`) reaches the server without a restart. Nil
|
|
54
|
+
# keeps the configuration the context was built with.
|
|
55
|
+
def initialize(configuration:, configuration_loader: nil)
|
|
48
56
|
@configuration = configuration
|
|
57
|
+
@configuration_loader = configuration_loader
|
|
58
|
+
@primed_key = nil
|
|
59
|
+
@buffer_sources = {}
|
|
60
|
+
@disk_feature_files = nil
|
|
61
|
+
@environment_features = nil
|
|
49
62
|
@generation = 0
|
|
50
63
|
@environment = nil
|
|
51
64
|
@cache_store = nil
|
|
@@ -77,6 +90,9 @@ module Rigor
|
|
|
77
90
|
dependency_source_index: project_scan.dependency_source_index,
|
|
78
91
|
synthetic_method_index: project_scan.synthetic_method_index,
|
|
79
92
|
project_patched_methods: project_scan.project_patched_methods,
|
|
93
|
+
# Issue #1700 — the configured paths' files overlaid with the open buffers; {#note_buffers}
|
|
94
|
+
# rebuilds this when an edit or a save changes the set.
|
|
95
|
+
required_features: (@environment_features = required_features),
|
|
80
96
|
**ProjectEnvironment.dependency_discovery_options(@configuration)
|
|
81
97
|
)
|
|
82
98
|
end
|
|
@@ -104,38 +120,104 @@ module Rigor
|
|
|
104
120
|
# The session is never persisted. Its state lives as long as this context, which is what a long-running
|
|
105
121
|
# server needs, and writing it would race exactly the way the read-only {#cache_store} exists to avoid.
|
|
106
122
|
# Seeding is therefore one-directional: the terminal warms the server, not the reverse.
|
|
123
|
+
#
|
|
124
|
+
# Every round recomputes {Cache::IncrementalSnapshot.fingerprint} (the function the CLI's `--incremental`
|
|
125
|
+
# keys its snapshot on — one input list) and compares it with the one the session was primed with. A
|
|
126
|
+
# session's recheck only sees source-file digests, so an edit to a `pre_eval:` file, an auto-detected
|
|
127
|
+
# `sig/*.rbs`, a lockfile or `.rigor.yml` would otherwise keep serving answers computed from the old
|
|
128
|
+
# inputs until the server restarts (#1613). A change — including a fingerprint that newly cannot be
|
|
129
|
+
# computed — drops the session and the world built from the old inputs and re-baselines, so the round equals
|
|
130
|
+
# a cold run. The `effects:` identity rides along because `Configuration#to_h` omits `effects:`, so the
|
|
131
|
+
# fingerprint alone does not move on an `effects:` edit. A fingerprint that was ALREADY uncomputable at
|
|
132
|
+
# priming is not a change: that session keeps rechecking.
|
|
133
|
+
# {#generation} is deliberately NOT bumped: the round that notices the change is itself computed from the
|
|
134
|
+
# new inputs, and the publisher discards a round whose generation moved.
|
|
107
135
|
def project_diagnostics
|
|
136
|
+
reload_configuration
|
|
137
|
+
fingerprint = current_fingerprint
|
|
138
|
+
key = [fingerprint, Effects::Identity.digest(configuration: @configuration)]
|
|
139
|
+
rebaseline if @session_primed && key != @primed_key
|
|
108
140
|
session = (@incremental_session ||= build_incremental_session)
|
|
109
141
|
return session.recheck.diagnostics if @session_primed
|
|
110
142
|
|
|
111
143
|
@session_primed = true
|
|
144
|
+
@primed_key = key
|
|
112
145
|
diagnostics, = session.run_incremental(
|
|
113
146
|
snapshot: Cache::IncrementalSnapshot.new(root: @configuration.cache_path),
|
|
114
|
-
fingerprint:
|
|
115
|
-
configuration: @configuration, roots: @configuration.paths
|
|
116
|
-
),
|
|
147
|
+
fingerprint: fingerprint,
|
|
117
148
|
persist: false
|
|
118
149
|
)
|
|
119
150
|
diagnostics
|
|
120
151
|
end
|
|
121
152
|
|
|
153
|
+
# Issue #1700 — records the open buffers' text (absolute path => source) and rebuilds the environment
|
|
154
|
+
# when it changes the {Environment::RequiredFeatures} set the current one was built with: a buffer that
|
|
155
|
+
# gains or drops a `require "prime"` loads or unloads the vendored `prime` signatures. The server calls
|
|
156
|
+
# it on every open, change, save and close; neither `didSave` nor a `.rb` edit reaches
|
|
157
|
+
# `workspace/didChangeWatchedFiles`, which most clients send only for the files they were asked to watch.
|
|
158
|
+
# The configured paths are read once per environment and only the buffers are re-scanned, so a keystroke
|
|
159
|
+
# costs a substring test per open buffer.
|
|
160
|
+
def note_buffers(sources)
|
|
161
|
+
@buffer_sources = sources
|
|
162
|
+
return if @environment.nil? || required_features == @environment_features
|
|
163
|
+
|
|
164
|
+
invalidate!
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# The required-feature set for the configured paths' files on disk, each open buffer read in place of its
|
|
168
|
+
# file.
|
|
169
|
+
def required_features
|
|
170
|
+
disk = (@disk_feature_files ||= Environment::RequiredFeatures.feature_files(@configuration))
|
|
171
|
+
found = disk.reject { |path, _| @buffer_sources.key?(path) }.values.flatten
|
|
172
|
+
found += @buffer_sources.values.flat_map { |source| Environment::RequiredFeatures.features_of(source) }
|
|
173
|
+
found.uniq.sort.freeze
|
|
174
|
+
end
|
|
175
|
+
|
|
122
176
|
# Drops every cached collaborator and bumps the generation. The next reader rebuilds from scratch.
|
|
123
177
|
# Triggered by `workspace/didChangeWatchedFiles` for project source files and by
|
|
124
178
|
# `workspace/didChangeConfiguration`.
|
|
125
179
|
def invalidate!
|
|
126
180
|
@generation += 1
|
|
181
|
+
reset_world
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
# Drops what was built from the previous inputs, without touching {#generation}.
|
|
187
|
+
def reset_world
|
|
127
188
|
@environment = nil
|
|
189
|
+
@environment_features = nil
|
|
190
|
+
@disk_feature_files = nil
|
|
128
191
|
@project_scan = nil
|
|
129
192
|
# The session's per-file cache was computed against the old environment / project scan, so it cannot
|
|
130
193
|
# outlive them. The next save round primes a fresh one.
|
|
131
194
|
@incremental_session = nil
|
|
132
195
|
@session_primed = false
|
|
196
|
+
@primed_key = nil
|
|
133
197
|
# Cache store stays — it's content-addressed; a stale env build won't be served because the file
|
|
134
198
|
# digest mixed into the cache key has changed.
|
|
135
199
|
nil
|
|
136
200
|
end
|
|
137
201
|
|
|
138
|
-
|
|
202
|
+
# A changed configuration may also move `cache_path`, so the store goes too (unlike {#invalidate!}).
|
|
203
|
+
def rebaseline
|
|
204
|
+
reset_world
|
|
205
|
+
@cache_store = nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def current_fingerprint
|
|
209
|
+
Cache::IncrementalSnapshot.fingerprint(configuration: @configuration, roots: @configuration.paths)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# A load that raises (a half-typed `.rigor.yml`) keeps the previous configuration: the editor must not
|
|
213
|
+
# lose its diagnostics to a transient parse error, and the next round retries.
|
|
214
|
+
def reload_configuration
|
|
215
|
+
return unless @configuration_loader
|
|
216
|
+
|
|
217
|
+
@configuration = @configuration_loader.call
|
|
218
|
+
rescue StandardError
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
139
221
|
|
|
140
222
|
# The session shares this context's warm Environment, so a round does not rebuild the RBS universe. It
|
|
141
223
|
# deliberately does NOT share the prebuilt ProjectScan: a round runs after a save, i.e. after the file
|
|
@@ -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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../hashing/xxh3"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Lens
|
|
7
|
+
# A site's anchor and contract-comment span (docs/internal-spec/lens.md § Anchors). The hashed material is the
|
|
8
|
+
# contract comment lines above the declaration (`#:`, `@rbs`, `@extrbs` and their continuation lines), the
|
|
9
|
+
# declaration's own bytes, and a `#:` comment trailing its last line. Documentation prose is not part of it.
|
|
10
|
+
class Anchor
|
|
11
|
+
Result = Data.define(:token, :line, :contract_comments)
|
|
12
|
+
|
|
13
|
+
# rbs-inline's annotation grammar (`#:`, `#[`, `#\s*@rbs`) plus `@extrbs`, matched from the `#`.
|
|
14
|
+
CONTRACT_COMMENT = /\A#(?::|\[|\s*@rbs\b|\s*@extrbs\b)/
|
|
15
|
+
# A `#:nodoc:`-style RDoc directive, which the rbs-inline plugin neutralises rather than reads.
|
|
16
|
+
RDOC_DIRECTIVE = /\A#:[a-z_][\w-]*:/
|
|
17
|
+
# A kept line that later comment lines may continue, with the spaces between its `#` and the tag.
|
|
18
|
+
CONTINUED_TAG = /\A#( *)@(?:rbs|extrbs)\b/
|
|
19
|
+
|
|
20
|
+
# @param source — the file's bytes (binary String)
|
|
21
|
+
# @param comments — the file's `Prism::Comment` list
|
|
22
|
+
def initialize(source, comments)
|
|
23
|
+
@source = source.b
|
|
24
|
+
@line_starts = [0]
|
|
25
|
+
@source.each_byte.with_index { |byte, index| @line_starts << (index + 1) if byte == 10 }
|
|
26
|
+
@comment_by_line = {}
|
|
27
|
+
comments.each do |comment|
|
|
28
|
+
next unless comment.is_a?(Prism::InlineComment)
|
|
29
|
+
|
|
30
|
+
@comment_by_line[comment.location.start_line] = comment
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The anchor of the declaration spanning bytes `start_offset...end_offset` and lines `start_line..end_line`.
|
|
35
|
+
def for_span(start_offset, end_offset, start_line, end_line)
|
|
36
|
+
leading = leading_contract_comments(start_line)
|
|
37
|
+
trailing = trailing_contract_comment(end_line, end_offset)
|
|
38
|
+
material = +"".b
|
|
39
|
+
leading.each { |comment| material << line_text_from(comment.location.start_offset) << "\n" }
|
|
40
|
+
material << @source.byteslice(start_offset, end_offset - start_offset)
|
|
41
|
+
material << "\n" << line_text_from(trailing.location.start_offset) if trailing
|
|
42
|
+
line = leading.empty? ? start_line : leading.first.location.start_line
|
|
43
|
+
Result.new(token: "#{line}:#{Hashing::XXH3.anchor_hash(material)}", line: line,
|
|
44
|
+
contract_comments: leading + [trailing].compact)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# The contract comments among the contiguous comment-only lines directly above `start_line`, in file order,
|
|
50
|
+
# with rbs-inline's continuation lines below a kept `@rbs` / `@extrbs` line.
|
|
51
|
+
def leading_contract_comments(start_line)
|
|
52
|
+
run = []
|
|
53
|
+
line = start_line - 1
|
|
54
|
+
while line >= 1 && (comment = comment_only_line(line))
|
|
55
|
+
run.unshift(comment)
|
|
56
|
+
line -= 1
|
|
57
|
+
end
|
|
58
|
+
kept = []
|
|
59
|
+
tag_indent = nil
|
|
60
|
+
run.each do |comment|
|
|
61
|
+
text = comment_text(comment)
|
|
62
|
+
if tag_indent && hash_indent(text) > tag_indent
|
|
63
|
+
kept << comment
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
tag_indent = nil
|
|
68
|
+
next unless contract_comment?(text)
|
|
69
|
+
|
|
70
|
+
kept << comment
|
|
71
|
+
continued = text.match(CONTINUED_TAG)
|
|
72
|
+
tag_indent = continued && continued[1].size
|
|
73
|
+
end
|
|
74
|
+
kept
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def trailing_contract_comment(end_line, end_offset)
|
|
78
|
+
comment = @comment_by_line[end_line]
|
|
79
|
+
return nil if comment.nil? || comment.location.start_offset < end_offset
|
|
80
|
+
|
|
81
|
+
text = comment_text(comment)
|
|
82
|
+
contract_comment?(text) && text.start_with?("#:") ? comment : nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def contract_comment?(text)
|
|
86
|
+
text.match?(CONTRACT_COMMENT) && !text.match?(RDOC_DIRECTIVE)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The spaces between a comment's `#` and its first non-space byte.
|
|
90
|
+
def hash_indent(text)
|
|
91
|
+
text[/\A#( *)/, 1].size
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The comment on `line` when nothing but whitespace precedes it.
|
|
95
|
+
def comment_only_line(line)
|
|
96
|
+
comment = @comment_by_line[line]
|
|
97
|
+
return nil if comment.nil?
|
|
98
|
+
|
|
99
|
+
prefix = @source.byteslice(@line_starts[line - 1], comment.location.start_offset - @line_starts[line - 1])
|
|
100
|
+
prefix.match?(/\A[ \t]*\z/) ? comment : nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The comment's text from its `#`, without a line terminator.
|
|
104
|
+
def comment_text(comment)
|
|
105
|
+
comment.location.slice.b.chomp.delete_suffix("\r")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The bytes from `offset` to the end of its line, without the line terminator.
|
|
109
|
+
def line_text_from(offset)
|
|
110
|
+
line_end = @source.index("\n", offset) || @source.bytesize
|
|
111
|
+
@source.byteslice(offset, line_end - offset).delete_suffix("\r")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "slot"
|
|
4
|
+
require_relative "../reflection"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Lens
|
|
8
|
+
# Reads a member's own RBS declaration into overloads with per-slot provenance (docs/internal-spec/lens.md
|
|
9
|
+
# § Members). A declaration the member inherits is not its contract; one written for its own class is, whether
|
|
10
|
+
# it comes from `sig/`, an inline comment, a plugin or a library.
|
|
11
|
+
class ContractReader
|
|
12
|
+
INFERRED_RETURN = "rigor:v1:inferred-return"
|
|
13
|
+
INFERRED_SIGNATURE = "rigor:v1:inferred-signature"
|
|
14
|
+
|
|
15
|
+
# `overloads` hold `{rbs, params, block, return}`; a return the contract leaves to inference is nil there,
|
|
16
|
+
# with `return_inferred` set. `buffers` are the declaring buffer names.
|
|
17
|
+
Contract = Data.define(:overloads, :provenance, :path, :return_inferred, :buffers)
|
|
18
|
+
|
|
19
|
+
def initialize(provenance)
|
|
20
|
+
@provenance = provenance
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The member's contract, or nil when it has none of its own. `name` is the Ruby method name.
|
|
24
|
+
def read(owner, scope, name, lookup_scope)
|
|
25
|
+
method = lookup(owner, scope, name, lookup_scope)
|
|
26
|
+
defs = method ? method.defs.select { |definition| definition.defined_in.to_s.delete_prefix("::") == owner } : []
|
|
27
|
+
return nil if defs.empty?
|
|
28
|
+
|
|
29
|
+
annotations = annotations_of(method, defs)
|
|
30
|
+
return nil if annotations.include?(INFERRED_SIGNATURE)
|
|
31
|
+
|
|
32
|
+
contract(defs, annotations)
|
|
33
|
+
rescue StandardError
|
|
34
|
+
unreadable("definition_build_failed", [])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def contract(defs, annotations)
|
|
40
|
+
buffers = defs.map { |definition| buffer_name(definition) }
|
|
41
|
+
classified = buffers.map { |buffer| @provenance.classify(buffer) }
|
|
42
|
+
return unreadable("not_computed", buffers) if classified.include?(nil)
|
|
43
|
+
|
|
44
|
+
return_inferred = annotations.include?(INFERRED_RETURN)
|
|
45
|
+
authored = annotations.select do |string|
|
|
46
|
+
string.start_with?("rigor:v1:") && !string.start_with?(INFERRED_RETURN)
|
|
47
|
+
end
|
|
48
|
+
# Each overload carries the provenance of the declaration it came from: a `sig/` overload extending a
|
|
49
|
+
# library declaration reads `sig`, the library one `library`.
|
|
50
|
+
overloads = defs.zip(classified).map do |definition, (provenance, path)|
|
|
51
|
+
overload(definition.type, provenance, path, return_inferred, authored)
|
|
52
|
+
end
|
|
53
|
+
provenance, path = classified.first
|
|
54
|
+
Contract.new(overloads: overloads, provenance: provenance, path: path,
|
|
55
|
+
return_inferred: return_inferred, buffers: buffers)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The method's and its own declarations' annotation strings: the plugin's defaulted-slot marks and authored
|
|
59
|
+
# `%a{…}`.
|
|
60
|
+
def annotations_of(method, defs)
|
|
61
|
+
strings = method.annotations.map(&:string) + defs.flat_map { |definition| definition.annotations.map(&:string) }
|
|
62
|
+
strings.map(&:strip).uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def buffer_name(definition)
|
|
66
|
+
location = definition.member&.location
|
|
67
|
+
location ? location.buffer&.name.to_s : ""
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def lookup(owner, scope, name, lookup_scope)
|
|
71
|
+
if scope == :singleton
|
|
72
|
+
Reflection.singleton_method_definition(owner, name.to_sym, scope: lookup_scope)
|
|
73
|
+
else
|
|
74
|
+
Reflection.instance_method_definition(owner, name.to_sym, scope: lookup_scope)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A declaration Rigor cannot read: its buffer name did not survive the environment cache
|
|
79
|
+
# (`not_computed`), or building the member's definition failed (`definition_build_failed`). The member has a
|
|
80
|
+
# contract, so it is not inferred; every slot is unavailable, and so are its parameters (`params: null`).
|
|
81
|
+
def unreadable(reason, buffers)
|
|
82
|
+
Contract.new(overloads: [{ "rbs" => nil, "params" => nil, "block" => nil,
|
|
83
|
+
"return" => Slot.unavailable(reason) }],
|
|
84
|
+
provenance: nil, path: nil, return_inferred: false, buffers: buffers)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def overload(method_type, provenance, path, return_inferred, annotations)
|
|
88
|
+
function = method_type.type
|
|
89
|
+
params = function.respond_to?(:required_positionals) ? params_of(function, provenance, path) : nil
|
|
90
|
+
block = method_type.block && { "required" => method_type.block.required,
|
|
91
|
+
"type" => Slot.from_rbs(method_type.block.type.to_s, provenance, path) }
|
|
92
|
+
prefix = annotations.map { |string| "%a{#{string}} " }.join
|
|
93
|
+
return_slot = return_inferred ? nil : Slot.from_rbs(function.return_type.to_s, provenance, path)
|
|
94
|
+
{ "rbs" => "#{prefix}#{method_type}", "params" => params, "block" => block, "return" => return_slot,
|
|
95
|
+
"return_rbs" => function.return_type.to_s }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def params_of(function, provenance, path)
|
|
99
|
+
(positional_params(function) + keyword_params(function)).map do |kind, name, param|
|
|
100
|
+
{ "name" => name&.to_s, "kind" => kind, "type" => Slot.from_rbs(param.type.to_s, provenance, path) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def positional_params(function)
|
|
105
|
+
function.required_positionals.map { |param| ["required", param.name, param] } +
|
|
106
|
+
function.optional_positionals.map { |param| ["optional", param.name, param] } +
|
|
107
|
+
[function.rest_positionals].compact.map { |param| ["rest", param.name, param] } +
|
|
108
|
+
function.trailing_positionals.map { |param| ["post", param.name, param] }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def keyword_params(function)
|
|
112
|
+
function.required_keywords.map { |name, param| ["keyreq", name, param] } +
|
|
113
|
+
function.optional_keywords.map { |name, param| ["key", name, param] } +
|
|
114
|
+
[function.rest_keywords].compact.map { |param| ["keyrest", param.name, param] }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|