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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "slot"
|
|
4
|
+
require_relative "parameters"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Lens
|
|
8
|
+
# Groups the sites of the requested files into members and fills their signatures (docs/internal-spec/lens.md
|
|
9
|
+
# § Members). A member is one `(owner, scope, Ruby method name)`; an `attr_accessor :a` site defines `a` and
|
|
10
|
+
# `a=`. An inferred slot comes from a site's body only when the member has exactly one project definition.
|
|
11
|
+
class Members
|
|
12
|
+
# @param sites — every {FileReader::Site} of the requested files
|
|
13
|
+
# @param omissions — every omission of the requested files
|
|
14
|
+
# @param contracts — `{[owner, scope, name] => ContractReader::Contract or nil}`
|
|
15
|
+
# @param elsewhere — callable `(owner, scope, name) -> [{path, line}]`: other project files defining it
|
|
16
|
+
# @param site_paths — `{site key => absolute path of its file}`, to match an inline contract's buffer
|
|
17
|
+
def initialize(sites:, omissions:, contracts:, elsewhere:, site_paths:)
|
|
18
|
+
@sites = sites
|
|
19
|
+
@omissions = omissions
|
|
20
|
+
@contracts = contracts
|
|
21
|
+
@elsewhere = elsewhere
|
|
22
|
+
@site_paths = site_paths
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build
|
|
26
|
+
groups = Hash.new { |hash, key| hash[key] = [] }
|
|
27
|
+
@sites.each do |site|
|
|
28
|
+
site.data["members"].each { |name| groups[[site.data["owner"], site.data["scope"], name]] << site }
|
|
29
|
+
end
|
|
30
|
+
result = groups.sort_by { |key, _| key }.map { |key, sites| member(key, sites) }
|
|
31
|
+
mark_inline_sites
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def member(key, sites)
|
|
38
|
+
owner, scope, name = key
|
|
39
|
+
contract = @contracts[key]
|
|
40
|
+
also = @elsewhere.call(owner, scope, name)
|
|
41
|
+
omitted = @omissions.count { |omission| [omission["owner"], omission["scope"], omission["name"]] == key }
|
|
42
|
+
single = sites.size == 1 && omitted.zero? && also.empty? ? sites.first : nil
|
|
43
|
+
{ "owner" => owner, "scope" => scope, "name" => name, "kind" => member_kind(sites),
|
|
44
|
+
"visibility" => common_visibility(sites), "sites" => sites.map { |site| site.data["key"] },
|
|
45
|
+
"also_defined_at" => also, "basis" => basis(contract, single),
|
|
46
|
+
"overloads" => overloads(contract, sites.first, name, single) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A member a `def` defines anywhere in the requested files is a method, whatever `attr_*` also names it.
|
|
50
|
+
def member_kind(sites)
|
|
51
|
+
sites.any? { |site| site.data["kind"] == "method" } ? "method" : "attribute"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def basis(contract, single)
|
|
55
|
+
return "contract" if contract
|
|
56
|
+
return "single_site" if single
|
|
57
|
+
|
|
58
|
+
"contested"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def common_visibility(sites)
|
|
62
|
+
values = sites.map { |site| site.data["visibility"] }.uniq
|
|
63
|
+
values.size == 1 ? values.first : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A contested member without a contract has no overloads: its definitions may differ in arity, and each
|
|
67
|
+
# method site carries its own parameter list.
|
|
68
|
+
def overloads(contract, site, name, single)
|
|
69
|
+
return [] if contract.nil? && single.nil?
|
|
70
|
+
|
|
71
|
+
overloads = contract ? contract_overloads(contract, single) : [body_overload(site, name, single)]
|
|
72
|
+
overloads.map { |overload| overload.merge("rbs" => rbs_unless_unavailable(overload)) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A contract's overloads. A defaulted return (`inferred-return`) is inferred as for a member without a
|
|
76
|
+
# contract, and its `untyped` parameters are unknown: Rigor cannot tell an authored `untyped` from a
|
|
77
|
+
# defaulted one.
|
|
78
|
+
def contract_overloads(contract, single)
|
|
79
|
+
contract.overloads.map do |overload|
|
|
80
|
+
next overload.except("return_rbs") unless contract.return_inferred
|
|
81
|
+
|
|
82
|
+
slot = inferred_return(single)
|
|
83
|
+
params = overload["params"]&.map do |param|
|
|
84
|
+
param["type"]["type"] == "untyped" ? param.merge("type" => Slot.unavailable("not_computed")) : param
|
|
85
|
+
end
|
|
86
|
+
rbs = overload["rbs"]
|
|
87
|
+
suffix = overload["return_rbs"]
|
|
88
|
+
rbs = rbs.delete_suffix(suffix) + return_spelling(slot) if rbs && suffix && rbs.end_with?(suffix)
|
|
89
|
+
overload.except("return_rbs").merge("rbs" => rbs, "params" => params, "return" => slot)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A member with no contract: its parameters as written, each `untyped` with ADR-75's undeclared-parameter
|
|
94
|
+
# cause, and the return from its one body.
|
|
95
|
+
def body_overload(site, name, single)
|
|
96
|
+
return attribute_overload(name, single) if site.data["kind"] == "attribute"
|
|
97
|
+
|
|
98
|
+
params, block, head = Parameters.read(site.node.parameters, yields: Parameters.yields?(site.node.body))
|
|
99
|
+
slot = inferred_return(single)
|
|
100
|
+
block_rbs = block ? " ?{ (?) -> untyped }" : ""
|
|
101
|
+
{ "rbs" => "#{head}#{block_rbs} -> #{return_spelling(slot)}", "params" => params, "block" => block,
|
|
102
|
+
"return" => slot }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# An attribute without a contract: Rigor does not report the ivar's type in revision 1.
|
|
106
|
+
def attribute_overload(name, _single)
|
|
107
|
+
unknown = Slot.unavailable("not_computed")
|
|
108
|
+
if name.end_with?("=")
|
|
109
|
+
param = { "name" => nil, "kind" => "required", "type" => Slot.unavailable("not_computed") }
|
|
110
|
+
{ "rbs" => nil, "params" => [param], "block" => nil, "return" => unknown }
|
|
111
|
+
else
|
|
112
|
+
{ "rbs" => nil, "params" => [], "block" => nil, "return" => unknown }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def inferred_return(single)
|
|
117
|
+
return Slot.unavailable("contested") unless single
|
|
118
|
+
|
|
119
|
+
body = single.data["body_return"]
|
|
120
|
+
return Slot.unavailable("not_computed") if body.nil?
|
|
121
|
+
return body if Slot.unavailable?(body)
|
|
122
|
+
|
|
123
|
+
body.merge("from_site" => single.data["key"])
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def return_spelling(slot)
|
|
127
|
+
spelled = Slot.rbs_spelling(slot)
|
|
128
|
+
spelled.include?(" | ") ? "(#{spelled})" : spelled
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# `rbs` is null when any slot of the overload is unavailable: an `untyped` there would read as an assertion.
|
|
132
|
+
def rbs_unless_unavailable(overload)
|
|
133
|
+
slots = [overload["return"], overload.dig("block", "type")] +
|
|
134
|
+
(overload["params"] || []).map { |param| param["type"] }
|
|
135
|
+
slots.any? { |slot| Slot.unavailable?(slot) } ? nil : overload["rbs"]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# A site reads `contract: inline` when its own contract comments produced an inline declaration of a member
|
|
139
|
+
# it defines.
|
|
140
|
+
def mark_inline_sites
|
|
141
|
+
@sites.each do |site|
|
|
142
|
+
path = @site_paths.fetch(site.data["key"])
|
|
143
|
+
asserted = site.contract_comments.any? { |comment| !comment.location.slice.match?(/\A#\s*@extrbs\b/) }
|
|
144
|
+
inline = asserted && site.data["members"].any? do |name|
|
|
145
|
+
contract = @contracts[[site.data["owner"], site.data["scope"], name]]
|
|
146
|
+
contract&.provenance == "inline" && contract.buffers.any? { |buffer| buffer.end_with?(":#{path}") }
|
|
147
|
+
end
|
|
148
|
+
site.data["contract"] = inline ? "inline" : nil
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "slot"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Lens
|
|
9
|
+
# A `def`'s parameter list as slots and as the RBS head sig-gen would write for an undeclared method.
|
|
10
|
+
module Parameters
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
UNDECLARED = "inferred_return_untyped"
|
|
14
|
+
|
|
15
|
+
# [params (nil for an argument-forwarding list), block or nil, "(…)" head]. A definition takes a block when
|
|
16
|
+
# it declares `&blk` or `yields` (its body yields).
|
|
17
|
+
def read(node, yields: false)
|
|
18
|
+
block = yields ? undeclared_block : nil
|
|
19
|
+
return [[], block, "()"] if node.nil?
|
|
20
|
+
return [nil, block, "(?)"] if node.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
21
|
+
|
|
22
|
+
entries = entries(node)
|
|
23
|
+
params = entries.map { |kind, name| { "name" => name&.to_s, "kind" => kind, "type" => undeclared } }
|
|
24
|
+
block = undeclared_block if node.block
|
|
25
|
+
[params, block, "(#{entries.map { |kind, name| spell(kind, name) }.join(', ')})"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# `[kind, name]` per parameter, in declaration order.
|
|
29
|
+
def entries(node)
|
|
30
|
+
node.requireds.map { |param| ["required", name_of(param)] } +
|
|
31
|
+
node.optionals.map { |param| ["optional", param.name] } +
|
|
32
|
+
rest(node.rest) +
|
|
33
|
+
node.posts.map { |param| ["post", name_of(param)] } +
|
|
34
|
+
node.keywords.map { |param| [keyword_kind(param), param.name] } +
|
|
35
|
+
keyword_rest(node.keyword_rest)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def undeclared
|
|
39
|
+
Slot.dynamic("inferred", UNDECLARED)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def undeclared_block
|
|
43
|
+
{ "required" => false, "type" => undeclared }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether a `def` body yields, outside the bodies whose `yield` belongs elsewhere: a nested `def`, a lambda,
|
|
47
|
+
# a `define_method` block.
|
|
48
|
+
def yields?(node)
|
|
49
|
+
return false if node.nil?
|
|
50
|
+
return true if node.is_a?(Prism::YieldNode)
|
|
51
|
+
return false if node.is_a?(Prism::DefNode) || node.is_a?(Prism::LambdaNode)
|
|
52
|
+
return false if node.is_a?(Prism::CallNode) && node.name == :define_method && node.receiver.nil?
|
|
53
|
+
|
|
54
|
+
node.compact_child_nodes.any? { |child| yields?(child) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The shape of a parameter list: names and kinds, with every type null (lens.md § Sites).
|
|
58
|
+
def shape(params, block)
|
|
59
|
+
[params&.map { |param| param.merge("type" => nil) }, block&.merge("type" => nil)]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def name_of(param)
|
|
63
|
+
param.respond_to?(:name) ? param.name : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def keyword_kind(param)
|
|
67
|
+
param.is_a?(Prism::RequiredKeywordParameterNode) ? "keyreq" : "key"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def rest(node)
|
|
71
|
+
node.is_a?(Prism::RestParameterNode) ? [["rest", node.name]] : []
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def keyword_rest(node)
|
|
75
|
+
node.is_a?(Prism::KeywordRestParameterNode) ? [["keyrest", node.name]] : []
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def spell(kind, name)
|
|
79
|
+
label = name ? " #{name}" : ""
|
|
80
|
+
case kind
|
|
81
|
+
when "required", "post" then "untyped#{label}"
|
|
82
|
+
when "optional" then "?untyped#{label}"
|
|
83
|
+
when "rest" then "*untyped#{label}"
|
|
84
|
+
when "keyreq" then "#{name}: untyped"
|
|
85
|
+
when "key" then "?#{name}: untyped"
|
|
86
|
+
when "keyrest" then "**untyped#{label}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../version"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Lens
|
|
7
|
+
# `context.plugins` (docs/internal-spec/lens.md § Context): the plugins a run loaded, by manifest id.
|
|
8
|
+
module Plugins
|
|
9
|
+
BUNDLED_PLUGINS = File.expand_path("../../../plugins", __dir__).freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# `[{id, version}]` by manifest id (the id slot provenance uses). The version is Rigor's for a plugin bundled
|
|
14
|
+
# with it, else the version of the gem whose files define the plugin class, else null. A plugin id is not a gem
|
|
15
|
+
# name: the bundled `rbs-inline` plugin shares its id with the upstream gem.
|
|
16
|
+
def loaded(environment)
|
|
17
|
+
plugins = environment&.plugin_registry&.plugins || []
|
|
18
|
+
rows = plugins.map { |plugin| { "id" => plugin.manifest.id.to_s, "version" => version(plugin) } }
|
|
19
|
+
rows.uniq.sort_by { |plugin| plugin["id"] }
|
|
20
|
+
rescue StandardError
|
|
21
|
+
[]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def version(plugin)
|
|
25
|
+
file = Object.const_source_location(plugin.class.name)&.first
|
|
26
|
+
return nil if file.nil?
|
|
27
|
+
|
|
28
|
+
absolute = File.expand_path(file)
|
|
29
|
+
return Rigor::VERSION if absolute.start_with?("#{BUNDLED_PLUGINS}/")
|
|
30
|
+
|
|
31
|
+
spec = Gem.loaded_specs.each_value.find do |gem|
|
|
32
|
+
absolute.start_with?("#{gem.full_gem_path}/") # rigor:disable undefined-method
|
|
33
|
+
end
|
|
34
|
+
spec&.version&.to_s
|
|
35
|
+
rescue StandardError
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "file_reader"
|
|
6
|
+
require_relative "contract_reader"
|
|
7
|
+
require_relative "members"
|
|
8
|
+
require_relative "plugins"
|
|
9
|
+
require_relative "slot"
|
|
10
|
+
require_relative "../analysis/incremental_session"
|
|
11
|
+
require_relative "../cache/incremental_snapshot"
|
|
12
|
+
require_relative "../cache/store"
|
|
13
|
+
require_relative "../sig_gen/alias_index"
|
|
14
|
+
require_relative "../sig_gen/type_elaborator"
|
|
15
|
+
|
|
16
|
+
module Rigor
|
|
17
|
+
module Lens
|
|
18
|
+
# The check-equivalent computation behind `rigor lens` (docs/internal-spec/lens.md § Computation). The session
|
|
19
|
+
# is the `--incremental` one rooted at the configuration's `paths:`, so it shares the snapshot with
|
|
20
|
+
# `rigor check --incremental`; the targets are forced into the run and read inside it, on the seeded index.
|
|
21
|
+
class Run
|
|
22
|
+
# One target's answer: `status` is `ok` or a failure reason; the rest is set only for `ok`.
|
|
23
|
+
Target = Data.define(:argument, :path, :absolute, :status, :reason, :reader, :diagnostics, :inputs)
|
|
24
|
+
Outcome = Data.define(:targets, :member_rows, :plugins, :warm)
|
|
25
|
+
|
|
26
|
+
# @param root — the project root (absolute); every path in the answer is relative to it
|
|
27
|
+
def initialize(configuration:, root:, arguments:, workers:)
|
|
28
|
+
@configuration = configuration
|
|
29
|
+
@root = root
|
|
30
|
+
@arguments = arguments
|
|
31
|
+
@argument_paths = arguments.to_set { |argument| File.expand_path(argument) }
|
|
32
|
+
@workers = workers
|
|
33
|
+
reset_captures
|
|
34
|
+
@capture_errors = {}
|
|
35
|
+
@plugins = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call
|
|
39
|
+
session = build_session
|
|
40
|
+
project = session.project_files.to_h { |path| [File.expand_path(path), path] }
|
|
41
|
+
resolved = mark_duplicates(@arguments.map { |argument| resolve(argument, project) })
|
|
42
|
+
forced = resolved.filter_map { |entry| entry[:session_path] }
|
|
43
|
+
session.force_targets = forced.uniq
|
|
44
|
+
diagnostics, warm = analyse(session, forced.uniq)
|
|
45
|
+
targets = resolved.map { |entry| target(entry, diagnostics, session) }
|
|
46
|
+
Outcome.new(targets: targets, member_rows: members(targets, session), plugins: @plugins, warm: warm)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_session
|
|
52
|
+
cache_root = @configuration.cache_path
|
|
53
|
+
Analysis::IncrementalSession.new(
|
|
54
|
+
configuration: @configuration,
|
|
55
|
+
cache_store: Cache::Store.new(root: cache_root, max_bytes: @configuration.cache_max_bytes),
|
|
56
|
+
workers: @workers, on_file_indexed: method(:capture)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A warm run is a recheck that forced the targets. A cold one is a baseline: a sequential baseline indexes
|
|
61
|
+
# every target in this process, a pooled one in workers where the callback never runs. The session also
|
|
62
|
+
# falls back to a baseline after a recheck whose seeds it no longer trusts, and the captures that recheck
|
|
63
|
+
# made are stale. So after a pooled baseline the captures are dropped and one forced recheck, on the
|
|
64
|
+
# baseline's fresh state, reads the targets again.
|
|
65
|
+
def analyse(session, forced)
|
|
66
|
+
fingerprint = Cache::IncrementalSnapshot.fingerprint(configuration: @configuration,
|
|
67
|
+
roots: @configuration.paths)
|
|
68
|
+
snapshot = Cache::IncrementalSnapshot.new(root: @configuration.cache_path)
|
|
69
|
+
diagnostics, warm = session.run_incremental(snapshot: snapshot, fingerprint: fingerprint)
|
|
70
|
+
missing = forced.reject { |path| @captured.key?(File.expand_path(path)) }
|
|
71
|
+
if (!warm && @workers.positive?) || !missing.empty?
|
|
72
|
+
reset_captures
|
|
73
|
+
diagnostics = session.recheck.diagnostics
|
|
74
|
+
end
|
|
75
|
+
[diagnostics, warm]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def reset_captures
|
|
79
|
+
@captured = {}
|
|
80
|
+
@contracts = {}
|
|
81
|
+
@site_paths = {}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A file requested again, in any spelling, is answered once: the later entries read `duplicate`, naming the
|
|
85
|
+
# argument that carries the answer, so every site key stays unique in the document.
|
|
86
|
+
def mark_duplicates(entries)
|
|
87
|
+
first = {}
|
|
88
|
+
entries.map do |entry|
|
|
89
|
+
next entry unless entry[:status] == "ok"
|
|
90
|
+
|
|
91
|
+
seen = first[entry[:absolute]]
|
|
92
|
+
first[entry[:absolute]] ||= entry[:argument]
|
|
93
|
+
next entry unless seen
|
|
94
|
+
|
|
95
|
+
entry.merge(status: "duplicate", reason: "the same file as #{seen}", session_path: nil)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resolve(argument, project)
|
|
100
|
+
absolute = File.expand_path(argument)
|
|
101
|
+
entry = { argument: argument, absolute: absolute, path: relative(absolute) }
|
|
102
|
+
return entry.merge(status: "not_found", reason: "no such file") unless File.file?(absolute)
|
|
103
|
+
return entry.merge(status: "not_ruby", reason: "not a Ruby source file") unless absolute.end_with?(".rb")
|
|
104
|
+
|
|
105
|
+
session_path = project[absolute]
|
|
106
|
+
return entry.merge(status: "outside_project", reason: "not under the configured paths:") unless session_path
|
|
107
|
+
|
|
108
|
+
entry.merge(status: "ok", session_path: session_path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The runner's index callback. An error here never reaches the run: it is recorded, and the target reads
|
|
112
|
+
# `failed` with the reason.
|
|
113
|
+
def capture(path, parse_result, scope, index)
|
|
114
|
+
absolute = File.expand_path(path)
|
|
115
|
+
return unless @argument_paths.include?(absolute)
|
|
116
|
+
|
|
117
|
+
@plugins = Plugins.loaded(scope.environment) if @plugins.empty?
|
|
118
|
+
read_target(absolute, parse_result, scope, index)
|
|
119
|
+
@capture_errors.delete(absolute)
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
@captured.delete(absolute)
|
|
122
|
+
@capture_errors[absolute] = "#{e.class}: #{e.message}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def read_target(absolute, parse_result, scope, index)
|
|
126
|
+
reader = FileReader.new(path: relative(absolute), parse_result: parse_result, index: index,
|
|
127
|
+
spell: speller(scope.environment)).read
|
|
128
|
+
reader.sites.each do |site|
|
|
129
|
+
@site_paths[site.data["key"]] = absolute
|
|
130
|
+
site.data["members"].each { |name| read_contract(site.data["owner"], site.data["scope"], name, scope) }
|
|
131
|
+
end
|
|
132
|
+
@captured[absolute] = reader
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def read_contract(owner, member_scope, name, scope)
|
|
136
|
+
key = [owner, member_scope, name]
|
|
137
|
+
return if @contracts.key?(key)
|
|
138
|
+
|
|
139
|
+
@contracts[key] = contract_reader(scope.environment).read(owner, member_scope.to_sym, name, scope)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def target(entry, diagnostics, session)
|
|
143
|
+
base = { argument: entry[:argument], path: entry[:path], absolute: entry[:absolute], status: entry[:status],
|
|
144
|
+
reason: entry[:reason], reader: nil, diagnostics: nil, inputs: nil }
|
|
145
|
+
return Target.new(**base) unless entry[:status] == "ok"
|
|
146
|
+
|
|
147
|
+
reader = @captured[entry[:absolute]]
|
|
148
|
+
own = diagnostics.select { |diagnostic| File.expand_path(diagnostic.path.to_s) == entry[:absolute] }
|
|
149
|
+
if reader
|
|
150
|
+
return Target.new(**base, reader: reader, diagnostics: own,
|
|
151
|
+
inputs: inputs(session, entry[:session_path]))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Target.new(**base, **uncaptured(entry[:absolute], own))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# A forced target the run did not read: one that does not parse stops before indexing; otherwise reading it
|
|
158
|
+
# failed, or the run never indexed it.
|
|
159
|
+
def uncaptured(absolute, own)
|
|
160
|
+
if parse_errors?(absolute)
|
|
161
|
+
{ status: "parse_error", reason: "the file does not parse", diagnostics: own }
|
|
162
|
+
else
|
|
163
|
+
{ status: "failed", reason: @capture_errors.fetch(absolute, "the file was not analysed") }
|
|
164
|
+
end
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
# The parse itself failed (an unsupported `target_ruby`, say): the run's own parse did too.
|
|
167
|
+
{ status: "failed", reason: "#{e.class}: #{e.message}" }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def parse_errors?(absolute)
|
|
171
|
+
Prism.parse_file(absolute, version: @configuration.target_ruby).errors.any?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def inputs(session, session_path)
|
|
175
|
+
rows = session.dependency_sources(session_path).filter_map do |source|
|
|
176
|
+
absolute = File.expand_path(source)
|
|
177
|
+
next if absolute == File.expand_path(session_path) || !File.file?(absolute)
|
|
178
|
+
|
|
179
|
+
{ "path" => relative(absolute), "digest" => Lens.digest(File.binread(absolute)) }
|
|
180
|
+
end
|
|
181
|
+
rows.sort_by { |input| input["path"] }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def members(targets, session)
|
|
185
|
+
# A file requested twice, in any spelling, contributes its declarations once.
|
|
186
|
+
readers = targets.filter_map { |target| target.reader && [target.absolute, target.reader] }.to_h
|
|
187
|
+
sites = readers.values.flat_map(&:sites)
|
|
188
|
+
omissions = readers.values.flat_map(&:omissions)
|
|
189
|
+
requested = readers.keys.to_set
|
|
190
|
+
Members.new(sites: sites, omissions: omissions, contracts: @contracts,
|
|
191
|
+
elsewhere: elsewhere(session, requested), site_paths: @site_paths).build
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Other project files that define a member, from the per-file discovery bundles the session keeps. The bundle
|
|
195
|
+
# paths are resolved once; each member then costs one lookup per bundle.
|
|
196
|
+
def elsewhere(session, requested)
|
|
197
|
+
bundles = session.seed_bundles.filter_map do |path, bundle|
|
|
198
|
+
absolute = File.expand_path(path)
|
|
199
|
+
[relative(absolute), bundle] unless requested.include?(absolute)
|
|
200
|
+
end
|
|
201
|
+
lambda do |owner, scope, name|
|
|
202
|
+
entries = bundles.filter_map do |path, bundle|
|
|
203
|
+
line = defined_line(bundle, owner, scope, name)
|
|
204
|
+
line == false ? nil : { "path" => path, "line" => line }
|
|
205
|
+
end
|
|
206
|
+
entries.sort_by { |entry| entry["path"] }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The line a bundle records for the member, nil when it defines the member without a recorded line (an
|
|
211
|
+
# `attr_*`), or false when it does not define it.
|
|
212
|
+
# A top-level `def` is recorded under discovery's own key, apart from `class Object`; both count for
|
|
213
|
+
# `Object#name`.
|
|
214
|
+
def defined_line(bundle, owner, scope, name)
|
|
215
|
+
owners = if owner == "Object" && scope == "instance"
|
|
216
|
+
[owner,
|
|
217
|
+
Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY]
|
|
218
|
+
else
|
|
219
|
+
[owner]
|
|
220
|
+
end
|
|
221
|
+
owners.each do |key|
|
|
222
|
+
line = defined_line_for(bundle, key, scope, name)
|
|
223
|
+
return line unless line == false
|
|
224
|
+
end
|
|
225
|
+
false
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def defined_line_for(bundle, owner, scope, name)
|
|
229
|
+
table = scope == "singleton" ? bundle[:singleton_def_sources] : bundle[:def_sources]
|
|
230
|
+
source = table&.dig(owner, name.to_sym)
|
|
231
|
+
return Integer(source.to_s.split(":").last) if source
|
|
232
|
+
|
|
233
|
+
kinds = bundle[:methods]&.dig(owner, name.to_sym)
|
|
234
|
+
return false if kinds.nil?
|
|
235
|
+
return nil if kinds == :both || kinds.to_s == scope
|
|
236
|
+
|
|
237
|
+
false
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def speller(environment)
|
|
241
|
+
@alias_index ||= SigGen::AliasIndex.build(environment: environment, signature_paths: signature_roots,
|
|
242
|
+
excluded_paths: plugin_signature_paths(environment))
|
|
243
|
+
lambda do |type, owner|
|
|
244
|
+
@alias_index.fold(SigGen::TypeElaborator.elaborate(type, environment: environment).erase_to_rbs, owner)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def contract_reader(environment)
|
|
249
|
+
@contract_reader ||= ContractReader.new(
|
|
250
|
+
Provenance.new(root: @root, signature_roots: signature_roots.map { |path| File.expand_path(path.to_s) },
|
|
251
|
+
plugin_roots: plugin_roots(environment))
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# `{absolute signature directory => plugin id}` for every loaded plugin that ships signatures.
|
|
256
|
+
def plugin_roots(environment)
|
|
257
|
+
plugins = environment&.plugin_registry&.plugins || []
|
|
258
|
+
plugins.each_with_object({}) do |plugin, roots|
|
|
259
|
+
Array(plugin.signature_paths).each { |path| roots[File.expand_path(path.to_s)] = plugin.manifest.id }
|
|
260
|
+
end
|
|
261
|
+
rescue StandardError
|
|
262
|
+
{}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def signature_roots
|
|
266
|
+
@signature_roots ||= Environment.project_signature_roots(@configuration.signature_paths)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def plugin_signature_paths(environment)
|
|
270
|
+
environment&.plugin_registry&.signature_paths || []
|
|
271
|
+
rescue StandardError
|
|
272
|
+
[]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def relative(absolute)
|
|
276
|
+
Pathname(absolute).relative_path_from(Pathname(@root)).to_s
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Lens
|
|
5
|
+
# Tagged type slots and RBS-buffer provenance (docs/internal-spec/lens.md § Slots).
|
|
6
|
+
module Slot
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def typed(type, provenance, path: nil, from_site: nil)
|
|
10
|
+
{ "state" => "typed", "type" => type, "provenance" => provenance, "path" => path, "from_site" => from_site }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def dynamic(provenance, cause, path: nil, from_site: nil)
|
|
14
|
+
{ "state" => "dynamic", "type" => "untyped", "provenance" => provenance, "path" => path,
|
|
15
|
+
"from_site" => from_site, "cause" => cause&.to_s }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unavailable(reason)
|
|
19
|
+
{ "state" => "unavailable", "reason" => reason }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def unavailable?(slot)
|
|
23
|
+
slot.is_a?(Hash) && slot["state"] == "unavailable"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A slot for an RBS type string read from a contract. `untyped` is a dynamic slot whose cause is the
|
|
27
|
+
# authored contract.
|
|
28
|
+
def from_rbs(type, provenance, path)
|
|
29
|
+
return dynamic(provenance, :explicit_untyped, path: path) if type == "untyped"
|
|
30
|
+
|
|
31
|
+
typed(type, provenance, path: path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The RBS type a slot puts in an overload's `rbs` string: an unavailable slot reads `untyped`.
|
|
35
|
+
def rbs_spelling(slot)
|
|
36
|
+
slot["state"] == "typed" ? slot["type"] : "untyped"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Maps an RBS member's buffer name to a provenance (§ Slots).
|
|
41
|
+
class Provenance
|
|
42
|
+
CACHED_BUFFER = "<cached>"
|
|
43
|
+
|
|
44
|
+
# @param root — the project root (absolute)
|
|
45
|
+
# @param signature_roots — the project signature roots (absolute)
|
|
46
|
+
# @param plugin_roots — `{absolute signature directory => plugin id}` of the loaded plugins
|
|
47
|
+
def initialize(root:, signature_roots:, plugin_roots:)
|
|
48
|
+
@root = root
|
|
49
|
+
@signature_roots = signature_roots
|
|
50
|
+
@plugin_roots = plugin_roots
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# [provenance, path] for a buffer name, or nil when the buffer cannot be classified.
|
|
54
|
+
def classify(buffer_name)
|
|
55
|
+
name = buffer_name.to_s
|
|
56
|
+
return ["inline", nil] if name.start_with?("virtual:rbs-inline:")
|
|
57
|
+
return ["plugin:#{name.split(':', 3)[1]}", nil] if name.start_with?("virtual:")
|
|
58
|
+
return nil if name.empty? || name == CACHED_BUFFER
|
|
59
|
+
|
|
60
|
+
absolute = File.expand_path(name, @root)
|
|
61
|
+
plugin = @plugin_roots.find { |root, _| under?(absolute, [root]) }
|
|
62
|
+
return ["plugin:#{plugin.last}", nil] if plugin
|
|
63
|
+
return ["sig", relative(absolute)] if under?(absolute, @signature_roots)
|
|
64
|
+
|
|
65
|
+
["library", nil]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def relative(absolute)
|
|
69
|
+
Pathname(absolute).relative_path_from(Pathname(@root)).to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def under?(absolute, roots)
|
|
75
|
+
roots.any? { |root| absolute == root || absolute.start_with?("#{root}/") }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/rigor/lens.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hashing/xxh3"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
# `rigor lens`: a declaration map of the requested files with type provenance, computed as `rigor check` computes
|
|
7
|
+
# them (ADR-113; the output contract is docs/internal-spec/lens.md).
|
|
8
|
+
module Lens
|
|
9
|
+
SCHEMA = "lens/v0"
|
|
10
|
+
# Bumped on every breaking change to the `lens/v0` document (docs/internal-spec/lens.md § Status).
|
|
11
|
+
REVISION = 1
|
|
12
|
+
CAPABILITIES = %w[class module method attribute body_return diagnostics inputs].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# A Prism location as the document's span object: 1-based lines, 0-based byte columns and offsets.
|
|
17
|
+
def span(location)
|
|
18
|
+
{ "start_line" => location.start_line, "start_column" => location.start_column,
|
|
19
|
+
"end_line" => location.end_line, "end_column" => location.end_column,
|
|
20
|
+
"start_offset" => location.start_offset, "end_offset" => location.end_offset }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The span from the first to the last of a site's contract comments, or nil when it has none.
|
|
24
|
+
def contract_span(comments)
|
|
25
|
+
return nil if comments.empty?
|
|
26
|
+
|
|
27
|
+
first = comments.first.location
|
|
28
|
+
last = comments.last.location
|
|
29
|
+
{ "start_line" => first.start_line, "start_column" => first.start_column, "end_line" => last.end_line,
|
|
30
|
+
"end_column" => last.end_column, "start_offset" => first.start_offset, "end_offset" => last.end_offset }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def digest(bytes)
|
|
34
|
+
"xxh3-64:#{Hashing::XXH3.file_hash(bytes)}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
require_relative "lens/run"
|
|
40
|
+
require_relative "lens/document"
|