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
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "project_patched_methods"
|
|
6
|
+
require_relative "project_patched_definedness_guard"
|
|
6
7
|
require_relative "../analysis/dependency_source_inference/return_type_heuristic"
|
|
8
|
+
require_relative "../source/alias_names"
|
|
7
9
|
require_relative "../source/constant_path"
|
|
8
10
|
require_relative "../source/node_children"
|
|
9
11
|
|
|
@@ -11,7 +13,8 @@ module Rigor
|
|
|
11
13
|
module Inference
|
|
12
14
|
# ADR-17 slice 2 — pre-pass scanner. Walks every file the user listed under `pre_eval:` and
|
|
13
15
|
# harvests every `def` / `def self.` declaration inside a class / module body into a
|
|
14
|
-
# {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier
|
|
16
|
+
# {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier, together with every
|
|
17
|
+
# `alias new old` / `alias_method :new, :old` there (issue #1702, read through {Source::AliasNames}).
|
|
15
18
|
#
|
|
16
19
|
# The walker is intentionally a strict subset of {Rigor::Inference::ScopeIndexer}'s machinery: it
|
|
17
20
|
# only needs `class C; def m; end; end` shape recognition, not full inference. Parse errors degrade
|
|
@@ -29,6 +32,17 @@ module Rigor
|
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
|
|
35
|
+
# An alias the walk met, before {resolve_aliases} knows whether its old name is one the patch files define.
|
|
36
|
+
PendingAlias = Data.define(:class_name, :new_name, :old_name, :kind, :source_path, :source_line)
|
|
37
|
+
private_constant :PendingAlias
|
|
38
|
+
|
|
39
|
+
# A `def` written under a definedness guard (`def foo ... unless method_defined?(:foo)`): a polyfill that runs
|
|
40
|
+
# only when nothing bound the name before it, so it never replaces an earlier binding.
|
|
41
|
+
GuardedDef = Data.define(:entry) do
|
|
42
|
+
def source_path = entry.source_path
|
|
43
|
+
end
|
|
44
|
+
private_constant :GuardedDef
|
|
45
|
+
|
|
32
46
|
module_function
|
|
33
47
|
|
|
34
48
|
# @param paths — absolute paths to the pre-eval files. The runner has already
|
|
@@ -39,10 +53,11 @@ module Rigor
|
|
|
39
53
|
# users editing a monkey-patch file see the in-flight version in their analysis.
|
|
40
54
|
# @return the populated registry plus any per-file warnings.
|
|
41
55
|
def scan(paths, buffer: nil)
|
|
42
|
-
|
|
56
|
+
collected = []
|
|
43
57
|
diagnostics = []
|
|
44
58
|
census = Set.new
|
|
45
|
-
paths.each { |path| scan_file(path,
|
|
59
|
+
paths.each { |path| scan_file(path, collected, diagnostics, buffer, census) }
|
|
60
|
+
entries = resolve_aliases(collected)
|
|
46
61
|
diagnostics.concat(duplicate_declaration_diagnostics(entries))
|
|
47
62
|
Result.new(
|
|
48
63
|
registry: ProjectPatchedMethods.new(entries: entries, write_census: census.freeze),
|
|
@@ -51,33 +66,75 @@ module Rigor
|
|
|
51
66
|
end
|
|
52
67
|
|
|
53
68
|
# ADR-17 § "Failure modes" — when two pre-eval entries declare the same `(class_name, method_name,
|
|
54
|
-
# kind)` triple,
|
|
55
|
-
#
|
|
56
|
-
#
|
|
69
|
+
# kind)` triple, the last-listed one wins, as at runtime where the later `def` replaces the earlier. Emit
|
|
70
|
+
# one `:info` `pre-eval.duplicate-declaration` diagnostic per SHADOWED entry, reported on that earlier
|
|
71
|
+
# declaration (the declaration that never takes effect) and naming the
|
|
72
|
+
# winning declaration it yields to.
|
|
57
73
|
def duplicate_declaration_diagnostics(entries)
|
|
58
|
-
|
|
59
|
-
entries.
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
seen[key] = entry
|
|
76
|
-
end
|
|
74
|
+
winners = entries.to_h { |entry| [[entry.class_name, entry.method_name, entry.kind], entry] }
|
|
75
|
+
entries.filter_map do |entry|
|
|
76
|
+
winner = winners[[entry.class_name, entry.method_name, entry.kind]]
|
|
77
|
+
next if winner.equal?(entry)
|
|
78
|
+
|
|
79
|
+
build_diagnostic(
|
|
80
|
+
path: entry.source_path,
|
|
81
|
+
line: entry.source_line,
|
|
82
|
+
column: 1,
|
|
83
|
+
severity: :info,
|
|
84
|
+
rule: "pre-eval.duplicate-declaration",
|
|
85
|
+
message: "pre-eval duplicate declaration: " \
|
|
86
|
+
"#{entry.class_name}##{entry.method_name} " \
|
|
87
|
+
"(#{entry.kind}) is declared again at " \
|
|
88
|
+
"#{winner.source_path}:#{winner.source_line}. " \
|
|
89
|
+
"The last-listed declaration wins; this entry is shadowed."
|
|
90
|
+
)
|
|
77
91
|
end
|
|
78
92
|
end
|
|
79
93
|
private_class_method :duplicate_declaration_diagnostics
|
|
80
94
|
|
|
95
|
+
# Turns each {PendingAlias} into the entry it publishes, walking the bindings in configuration-then-source
|
|
96
|
+
# order as Ruby runs them. An alias binds to the body its old name has WHEN IT RUNS, so only a `def` or alias
|
|
97
|
+
# earlier in that order may supply its return type: `alias to_m to_modint` after `def to_modint` answers what
|
|
98
|
+
# `to_modint` does, while `alias_method :orig_succ, :succ` ahead of a patch's `def succ` is the method
|
|
99
|
+
# Integer already had. An alias whose old name nothing earlier binds records that name as `alias_of`, for the
|
|
100
|
+
# dispatcher to answer with the class's existing method (`alias old_plus +` on Integer), or with
|
|
101
|
+
# `Dynamic[top]` when nothing knows the name: the alias is still published, since a call to it is not the
|
|
102
|
+
# undefined method a dropped entry would report.
|
|
103
|
+
#
|
|
104
|
+
# Within one file the later binding of a name replaces the earlier, as at runtime: the alias-method-chain
|
|
105
|
+
# idiom (`def foo`, `alias_method :foo_without_x, :foo`, `alias_method :foo, :foo_with_x`) leaves `foo` the
|
|
106
|
+
# alias. Bindings of one name in DIFFERENT files are what `pre-eval.duplicate-declaration` reports, and the
|
|
107
|
+
# registry keeps the last of those, as Ruby does.
|
|
108
|
+
def resolve_aliases(collected)
|
|
109
|
+
bound = {}
|
|
110
|
+
per_file = collected.group_by(&:source_path).values.map do |items|
|
|
111
|
+
final = {}
|
|
112
|
+
items.each do |item|
|
|
113
|
+
entry = item.is_a?(PendingAlias) ? alias_entry(item, bound) : item
|
|
114
|
+
entry = entry.entry if entry.is_a?(GuardedDef)
|
|
115
|
+
key = [entry.class_name, entry.method_name, entry.kind]
|
|
116
|
+
next if item.is_a?(GuardedDef) && bound.key?(key)
|
|
117
|
+
|
|
118
|
+
bound[key] = entry
|
|
119
|
+
final.delete(key)
|
|
120
|
+
final[key] = entry
|
|
121
|
+
end
|
|
122
|
+
final.values
|
|
123
|
+
end
|
|
124
|
+
per_file.flatten(1)
|
|
125
|
+
end
|
|
126
|
+
private_class_method :resolve_aliases
|
|
127
|
+
|
|
128
|
+
def alias_entry(item, bound)
|
|
129
|
+
target = bound[[item.class_name, item.old_name, item.kind]]
|
|
130
|
+
ProjectPatchedMethods::Entry.new(
|
|
131
|
+
class_name: item.class_name, method_name: item.new_name, kind: item.kind,
|
|
132
|
+
source_path: item.source_path, source_line: item.source_line,
|
|
133
|
+
return_type: target&.return_type, alias_of: target ? target.alias_of : item.old_name
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
private_class_method :alias_entry
|
|
137
|
+
|
|
81
138
|
def scan_file(path, entries, diagnostics, buffer = nil, census = Set.new)
|
|
82
139
|
physical = buffer ? buffer.resolve(path) : path
|
|
83
140
|
parse_result =
|
|
@@ -91,6 +148,7 @@ module Rigor
|
|
|
91
148
|
return
|
|
92
149
|
end
|
|
93
150
|
|
|
151
|
+
Thread.current[:rigor_guarded_defs] = DefinednessGuard.defs(parse_result.value)
|
|
94
152
|
walk_node(parse_result.value, [], false, path, entries)
|
|
95
153
|
# Issue #1367 — the file's `global.*` write facts: a patch file may alias a special or give a class the
|
|
96
154
|
# method a setter asks for, as any project file may.
|
|
@@ -104,6 +162,8 @@ module Rigor
|
|
|
104
162
|
"#{e.class}: #{e.message}. Pre-evaluation skipped for this file; " \
|
|
105
163
|
"the rest of the run proceeds."
|
|
106
164
|
)
|
|
165
|
+
ensure
|
|
166
|
+
Thread.current[:rigor_guarded_defs] = nil
|
|
107
167
|
end
|
|
108
168
|
private_class_method :scan_file
|
|
109
169
|
|
|
@@ -139,6 +199,13 @@ module Rigor
|
|
|
139
199
|
descend_singleton_class(node, qualified_prefix, source_path, entries)
|
|
140
200
|
when Prism::DefNode
|
|
141
201
|
record_def_node(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
202
|
+
when Prism::AliasMethodNode
|
|
203
|
+
record_alias(node, Source::AliasNames.keyword_names(node), qualified_prefix, in_singleton_class,
|
|
204
|
+
source_path, entries)
|
|
205
|
+
when Prism::CallNode
|
|
206
|
+
record_alias(node, Source::AliasNames.alias_method_call_names(node), qualified_prefix,
|
|
207
|
+
in_singleton_class, source_path, entries)
|
|
208
|
+
walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
142
209
|
else
|
|
143
210
|
walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
144
211
|
end
|
|
@@ -179,13 +246,28 @@ module Rigor
|
|
|
179
246
|
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
180
247
|
line = node.location&.start_line || 1
|
|
181
248
|
return_type = Analysis::DependencySourceInference::ReturnTypeHeuristic.extract(node)
|
|
182
|
-
|
|
249
|
+
entry = ProjectPatchedMethods::Entry.new(
|
|
183
250
|
class_name: class_name, method_name: node.name, kind: kind,
|
|
184
251
|
source_path: source_path, source_line: line,
|
|
185
252
|
return_type: return_type
|
|
186
253
|
)
|
|
254
|
+
entries << (Thread.current[:rigor_guarded_defs]&.key?(node) ? GuardedDef.new(entry: entry) : entry)
|
|
187
255
|
end
|
|
188
256
|
private_class_method :record_def_node
|
|
257
|
+
|
|
258
|
+
# `names` is the alias's `[new_name, old_name]`, or nil for a computed name or a call that is not an alias.
|
|
259
|
+
# The side follows the body it is written in, as a `def`'s does: `alias` and `alias_method` inside
|
|
260
|
+
# `class << self` both bind on the singleton.
|
|
261
|
+
def record_alias(node, names, qualified_prefix, in_singleton_class, source_path, entries)
|
|
262
|
+
return if names.nil? || qualified_prefix.empty?
|
|
263
|
+
|
|
264
|
+
entries << PendingAlias.new(
|
|
265
|
+
class_name: qualified_prefix.join("::"), new_name: names.first, old_name: names.last,
|
|
266
|
+
kind: in_singleton_class ? :singleton : :instance,
|
|
267
|
+
source_path: source_path, source_line: node.location&.start_line || 1
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
private_class_method :record_alias
|
|
189
271
|
end
|
|
190
272
|
end
|
|
191
273
|
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/alias_names"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# ADR-121 WD7 (issues #1796, #1799) — what a `refine` body tells the refinement table, read the one way the project
|
|
11
|
+
# walk (`ScopeIndexer#record_refinement_defs`) and the gem source walker
|
|
12
|
+
# (`Analysis::DependencySourceInference::Walker#walk_refine_body`) both use, so the two cannot drift. The table is
|
|
13
|
+
# `{refined class => {method => [refining modules]}}`; `Scope::DiscoveryIndex::REFINEMENT_WILDCARD` stands for
|
|
14
|
+
# what could not be read.
|
|
15
|
+
#
|
|
16
|
+
# A body is fully read when every call on its own `self` (the refinement module) is one that defines a name the
|
|
17
|
+
# walk can spell, or one that defines nothing: a `def`, an `alias` or `alias_method` of two literal names, a
|
|
18
|
+
# `define_method` of a literal name, an `attr_*` of literal names, an `undef`, a visibility call, or
|
|
19
|
+
# `Refinement#target`. Anything
|
|
20
|
+
# else (`import_methods`, `send`, a computed `define_method`, `include`, a nested `refine`, any other call on
|
|
21
|
+
# `self`) may define a name the walk cannot spell, so the body's names are incomplete and the table gets a
|
|
22
|
+
# names-wildcard row for each target. A call on another receiver cannot define a method on the refinement, and
|
|
23
|
+
# a call inside a `def` or `define_method` body runs on an instance of the refined class, so neither counts.
|
|
24
|
+
module RefineCensus
|
|
25
|
+
# `names`, every name the body is read to define, in textual order; `complete`, whether those are all of them;
|
|
26
|
+
# `defs`, the typed refined arm's bodies in textual order: `[name, Prism::DefNode]` for a `def` or an alias of
|
|
27
|
+
# an earlier `def`, and `[name, nil]` for any other definer (`define_method`, `attr_*`, `undef`, an alias of a
|
|
28
|
+
# name the body did not define earlier). A later definer replaces an earlier one, as Ruby's method table
|
|
29
|
+
# keeps the last, so a name whose last definer has no `DefNode` types as `Dynamic[top]`.
|
|
30
|
+
BodyReading = Struct.new(:names, :complete, :defs)
|
|
31
|
+
|
|
32
|
+
# Calls on the refinement that define nothing: visibility, with no argument, literal names, a `def`, or an
|
|
33
|
+
# allowlisted definer call.
|
|
34
|
+
VISIBILITY_CALLS = Set[:private, :public, :protected, :module_function, :private_constant].freeze
|
|
35
|
+
# `Refinement#target`, a reader of the refined class (the other `Refinement` method on Ruby 4.0.5 is
|
|
36
|
+
# `import_methods`).
|
|
37
|
+
READER_CALLS = Set[:target].freeze
|
|
38
|
+
# `attr_*` and whether each defines the reader and the writer.
|
|
39
|
+
ATTR_CALLS = {
|
|
40
|
+
attr_reader: [true, false].freeze, attr_writer: [false, true].freeze, attr_accessor: [true, true].freeze,
|
|
41
|
+
attr: [true, false].freeze
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# A3 — the calls whose String argument may hold `refine` as code or as a method name, and the two families
|
|
45
|
+
# whose `self` is their receiver (so `M.send(:refine, …)` runs `refine` on `M`).
|
|
46
|
+
SEND_CALLS = Set[:send, :__send__, :public_send].freeze
|
|
47
|
+
SELF_EVAL_CALLS = Set[:instance_eval, :class_eval, :module_eval, :instance_exec, :class_exec, :module_exec].freeze
|
|
48
|
+
NAMING_CALLS = Set[
|
|
49
|
+
:method, :public_method, :singleton_method, :instance_method, :public_instance_method, :define_method,
|
|
50
|
+
:define_singleton_method, :alias_method
|
|
51
|
+
].freeze
|
|
52
|
+
REFINE_WORD = /\brefine\b/
|
|
53
|
+
|
|
54
|
+
private_constant :VISIBILITY_CALLS, :READER_CALLS, :ATTR_CALLS, :REFINE_WORD
|
|
55
|
+
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
def wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
59
|
+
|
|
60
|
+
# §2.2 — what `body` (a refine block's body, or nil) defines on the refined class.
|
|
61
|
+
def read_body(body)
|
|
62
|
+
reading = BodyReading.new([], true, [])
|
|
63
|
+
scan_body(body, reading, {}, true) if body
|
|
64
|
+
reading.names.uniq!
|
|
65
|
+
reading
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Records `reading`'s names for each of `targets` under `refining` into `table` (nil starts one), plus a
|
|
69
|
+
# names-wildcard row per target when the body's names are incomplete. Returns the table.
|
|
70
|
+
def record_rows(table, targets, reading, refining)
|
|
71
|
+
table ||= {}
|
|
72
|
+
targets.each do |class_name|
|
|
73
|
+
reading.names.each { |name| add_row(table, class_name, name, refining) }
|
|
74
|
+
add_row(table, class_name, wildcard, refining) unless reading.complete
|
|
75
|
+
end
|
|
76
|
+
table
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# One `{class_name => {method_key => [refining]}}` entry, `refining` once.
|
|
80
|
+
def add_row(table, class_name, method_key, refining)
|
|
81
|
+
modules = ((table[class_name] ||= {})[method_key] ||= [])
|
|
82
|
+
modules << refining unless modules.include?(refining)
|
|
83
|
+
table
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A3 — a `:refine` Symbol literal: it may name `Module#refine` for `send`, `alias_method`, `method`, ….
|
|
87
|
+
def refine_symbol?(node) = node.is_a?(Prism::SymbolNode) && node.unescaped == "refine"
|
|
88
|
+
|
|
89
|
+
# A3 — a String literal (or a heredoc's literal part) holding the word `refine`, as code an eval runs or as a
|
|
90
|
+
# method name.
|
|
91
|
+
def refine_string?(node)
|
|
92
|
+
case node
|
|
93
|
+
# As bytes: a literal whose bytes are not valid in its encoding (`"\xff …"`) must not raise.
|
|
94
|
+
when Prism::StringNode then REFINE_WORD.match?(node.unescaped.b)
|
|
95
|
+
when Prism::InterpolatedStringNode then node.parts.any? { |part| refine_string?(part) }
|
|
96
|
+
else false
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A3 — does a String argument of `call` count: an eval, `send` or method-naming call?
|
|
101
|
+
def string_literal_call?(call)
|
|
102
|
+
name = call.name
|
|
103
|
+
name == :eval || SELF_EVAL_CALLS.include?(name) || SEND_CALLS.include?(name) || NAMING_CALLS.include?(name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A3 — the `self` a refine literal argument of `call` runs `refine` on: `:receiver` for a `send` or `*_eval`
|
|
107
|
+
# / `*_exec` call (the receiver, or the caller's `self` when it has none or it is `self`), `:unknown` for an
|
|
108
|
+
# `eval` on a binding, `:self` for everything else (the caller's `self`).
|
|
109
|
+
def literal_target(call)
|
|
110
|
+
receiver = call.receiver
|
|
111
|
+
self_receiver = receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
112
|
+
if SEND_CALLS.include?(call.name) || SELF_EVAL_CALLS.include?(call.name)
|
|
113
|
+
self_receiver ? :self : :receiver
|
|
114
|
+
elsif call.name == :eval
|
|
115
|
+
self_receiver ? :self : :unknown
|
|
116
|
+
else
|
|
117
|
+
:self
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# An implicit- or `self`-receiver call: one whose `self` is the walk's `self`.
|
|
122
|
+
def self_call?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
123
|
+
|
|
124
|
+
# `classify` is false inside a `define_method` block or its arguments, whose calls run on an instance of the
|
|
125
|
+
# refined class (or before the method exists), so they define nothing on the refinement. `latest` maps each
|
|
126
|
+
# name to the last `def` the body wrote for it so far, which an alias copies.
|
|
127
|
+
def scan_body(node, reading, latest, classify)
|
|
128
|
+
case node
|
|
129
|
+
when Prism::DefNode
|
|
130
|
+
record_def(node, reading, latest) if node.receiver.nil?
|
|
131
|
+
return
|
|
132
|
+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
133
|
+
return
|
|
134
|
+
when Prism::AliasMethodNode
|
|
135
|
+
return record_alias(Source::AliasNames.keyword_names(node), reading, latest)
|
|
136
|
+
when Prism::UndefNode
|
|
137
|
+
return record_undef(node, reading, latest)
|
|
138
|
+
when Prism::CallNode
|
|
139
|
+
if classify && self_call?(node)
|
|
140
|
+
classify_call(node, reading, latest)
|
|
141
|
+
classify = node.name != :define_method
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
node.rigor_each_child { |child| scan_body(child, reading, latest, classify) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def record_def(node, reading, latest)
|
|
149
|
+
reading.names << node.name
|
|
150
|
+
reading.defs << [node.name, node]
|
|
151
|
+
latest[node.name] = node
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# `alias new old` / `alias_method :new, :old`: `new` is defined; it copies `old`'s body when the refine body
|
|
155
|
+
# wrote `old` earlier (Ruby copies at the alias, so a later `def old` does not move it), and otherwise it
|
|
156
|
+
# copies the refined class's own method, whose body the walk does not hold.
|
|
157
|
+
def record_alias(pair, reading, latest)
|
|
158
|
+
return reading.complete = false if pair.nil?
|
|
159
|
+
|
|
160
|
+
new_name, old_name = pair
|
|
161
|
+
reading.names << new_name
|
|
162
|
+
def_node = latest[old_name]
|
|
163
|
+
return record_bodiless(new_name, reading, latest) if def_node.nil?
|
|
164
|
+
|
|
165
|
+
reading.defs << [new_name, def_node]
|
|
166
|
+
latest[new_name] = def_node
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A name a definer with no `DefNode` defines: it replaces any earlier `def` of the name, so the typed arm
|
|
170
|
+
# finds no body for it (`refine(String) { def center(a) = 1; define_method(:center) { |a| "dm" } }` answers
|
|
171
|
+
# `"dm"` on Ruby 4.0.5), and an alias of it copies no body either.
|
|
172
|
+
def record_bodiless(name, reading, latest)
|
|
173
|
+
reading.names << name
|
|
174
|
+
reading.defs << [name, nil]
|
|
175
|
+
latest.delete(name)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def record_undef(node, reading, latest)
|
|
179
|
+
node.names.each do |name|
|
|
180
|
+
symbol = literal_name(name)
|
|
181
|
+
symbol ? record_bodiless(symbol, reading, latest) : reading.complete = false
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Classifies a call on the refinement.
|
|
186
|
+
def classify_call(node, reading, latest)
|
|
187
|
+
name = node.name
|
|
188
|
+
arguments = node.arguments&.arguments || []
|
|
189
|
+
if (attr = ATTR_CALLS[name])
|
|
190
|
+
record_attr(arguments, attr, reading, latest)
|
|
191
|
+
elsif name == :define_method
|
|
192
|
+
record_define_method(arguments, reading, latest)
|
|
193
|
+
elsif name == :alias_method
|
|
194
|
+
record_alias(Source::AliasNames.alias_method_call_names(node), reading, latest)
|
|
195
|
+
else
|
|
196
|
+
reading.complete = false unless defines_nothing?(node, arguments)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# A visibility call with no argument, literal names, a `def` or a call; `Refinement#target` with none.
|
|
201
|
+
def defines_nothing?(node, arguments)
|
|
202
|
+
return arguments.all? { |argument| visibility_argument?(argument) } if VISIBILITY_CALLS.include?(node.name)
|
|
203
|
+
|
|
204
|
+
READER_CALLS.include?(node.name) && arguments.empty? && node.block.nil?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def record_attr(arguments, (reader, writer), reading, latest)
|
|
208
|
+
arguments.each do |argument|
|
|
209
|
+
name = literal_name(argument)
|
|
210
|
+
next reading.complete = false if name.nil?
|
|
211
|
+
|
|
212
|
+
record_bodiless(name, reading, latest) if reader
|
|
213
|
+
record_bodiless(:"#{name}=", reading, latest) if writer
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def record_define_method(arguments, reading, latest)
|
|
218
|
+
name = arguments.first && literal_name(arguments.first)
|
|
219
|
+
name ? record_bodiless(name, reading, latest) : reading.complete = false
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# `private`, `private :a, "b"`, `private def x`, `private attr_reader :x`: the nested call is classified on
|
|
223
|
+
# its own when the walk descends into it.
|
|
224
|
+
def visibility_argument?(argument)
|
|
225
|
+
argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode) || argument.is_a?(Prism::DefNode) ||
|
|
226
|
+
argument.is_a?(Prism::CallNode)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# A literal's name, or nil for none and for bytes no Symbol holds: `define_method("\xff")` raises
|
|
230
|
+
# EncodingError on Ruby 4.0.5, so the body is not read.
|
|
231
|
+
def literal_name(node)
|
|
232
|
+
return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
233
|
+
|
|
234
|
+
name = node.unescaped
|
|
235
|
+
name.to_sym if name&.valid_encoding?
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|