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
|
@@ -28,8 +28,11 @@ module Rigor
|
|
|
28
28
|
|
|
29
29
|
# One constant reference. `from` is the fully-qualified name of the innermost enclosing declaration, or
|
|
30
30
|
# nil for a reference written at file level — that is what makes the graph a reachability graph rather
|
|
31
|
-
# than a reference count (ADR-102 WD2).
|
|
32
|
-
#
|
|
31
|
+
# than a reference count (ADR-102 WD2). A reference in a declaration's own header — `class Sub < Base`,
|
|
32
|
+
# `Sub = Class.new(Base)` — is credited to that declaration, while `nesting` stays the outer scope it
|
|
33
|
+
# resolves in (#1720); that header case is the only one where `from` is not `nesting` joined. `role` is
|
|
34
|
+
# the referring FILE's role (WD8). `rooted` is true when the constant was written with a leading `::`
|
|
35
|
+
# (#625).
|
|
33
36
|
Reference = Data.define(:as_written, :nesting, :from, :role, :path, :line, :rooted) do
|
|
34
37
|
def initialize(rooted: false, **) = super
|
|
35
38
|
end
|
|
@@ -45,31 +48,83 @@ module Rigor
|
|
|
45
48
|
# children (#370). `config/recurring.yml` mentioning `Admin` is not evidence about
|
|
46
49
|
# `Admin::CollectionPolicy`; treating it as such demoted 18 unrelated Mastodon rows off one
|
|
47
50
|
# Afrikaans word ("Administrasie" contains "Admin").
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
# - `:leading` — `"V#{version}"` is the START of a name, not a namespace, so it can construct `V1_0`
|
|
52
|
+
# and `V2_2` as well as anything beneath them: a string-prefix match (#1734).
|
|
53
|
+
#
|
|
54
|
+
# `within` is set for a `const_get` on a known receiver — `Foo.const_get("Bar::#{k}")`, or an implicit
|
|
55
|
+
# receiver, `self` or `self.class`, which is the enclosing declaration written rooted: `prefix` is then
|
|
56
|
+
# relative to the receiver, a {Reference} that {Graph} resolves as it resolves any other before
|
|
57
|
+
# matching. A literal `name` keeps `within` too, and {Graph} looks it up from there (#1761). `inherit`
|
|
58
|
+
# is false when the call passes `inherit = false`, which confines the lookup to the receiver itself;
|
|
59
|
+
# otherwise the receiver's ancestors are searched as well. `role` is the referring file's (WD8).
|
|
60
|
+
DynamicUse = Data.define(:name, :prefix, :reason, :site, :path, :line, :scope, :within, :inherit, :role) do
|
|
61
|
+
def initialize(scope: :namespace, within: nil, inherit: true, role: :production, **) = super
|
|
50
62
|
|
|
51
63
|
# Whether this site's evidence reaches `fqn`.
|
|
52
64
|
def taints?(fqn)
|
|
53
65
|
return false if prefix.nil?
|
|
54
66
|
return fqn == prefix if scope == :exact
|
|
67
|
+
return fqn.start_with?(prefix) if scope == :leading
|
|
55
68
|
|
|
56
69
|
fqn == prefix || fqn.start_with?("#{prefix}::")
|
|
57
70
|
end
|
|
71
|
+
|
|
72
|
+
# The namespaces an interpolated site's `within` names, resolved by the block. A resolution of the whole
|
|
73
|
+
# written path is the anchor. {Graph#resolve} peels an unknown `Foo::Bar` to `Foo`, though, and
|
|
74
|
+
# anchoring there alone looked for `Foo::V*` and missed `Foo::Bar::V1`; the written name is then an
|
|
75
|
+
# anchor as well as the peeled one, since an undeclared receiver may be an alias of either. A receiver
|
|
76
|
+
# that does not resolve at all keeps its written name. Over-reaching is safe for a taint only; a
|
|
77
|
+
# literal anchors at {#receiver} alone.
|
|
78
|
+
def anchors
|
|
79
|
+
resolved = yield(within)
|
|
80
|
+
whole?(resolved) ? [resolved] : [within.as_written, resolved].compact
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The declaration `within` names when it resolves as WHOLE, or nil. A receiver resolved only by peeling
|
|
84
|
+
# its last segment is unknown: `const_get` never searches the receiver's lexical parent, so anchoring a
|
|
85
|
+
# literal there could resolve it to a dead constant and miss the live one.
|
|
86
|
+
def receiver
|
|
87
|
+
resolved = yield(within)
|
|
88
|
+
whole?(resolved) ? resolved : nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def whole?(resolved)
|
|
92
|
+
written = within.as_written
|
|
93
|
+
resolved == written || resolved&.end_with?("::#{written}")
|
|
94
|
+
end
|
|
58
95
|
end
|
|
59
96
|
|
|
60
97
|
Result = Data.define(:declarations, :references, :dynamic_uses)
|
|
61
98
|
|
|
62
99
|
# Roles a referring file can have (ADR-102 WD8). A reference edge carries its referrer's role so
|
|
63
100
|
# "used only by its own test" is a reportable category rather than a bucket boundary.
|
|
64
|
-
|
|
101
|
+
#
|
|
102
|
+
# End-to-end suites (`qa/`, `e2e/`, GitLab's QA; `features/`, cucumber) are test code too (#1751). They
|
|
103
|
+
# are matched at the project root only: `app/models/features/` is an ordinary namespace directory, and
|
|
104
|
+
# a false :test would hide a production reference. Tooling directories (`rubocop/`, `keeps/`,
|
|
105
|
+
# `tooling/`, `scripts/`) stay :production on purpose — deleting a class they use breaks the tooling.
|
|
106
|
+
#
|
|
107
|
+
# The role is decided from the path RELATIVE to `root` (the project root), never from the absolute
|
|
108
|
+
# path: `/home/me/test/app/lib/a.rb` is not test code because a directory above the project is called
|
|
109
|
+
# `test`. With no `root` the path is taken as already relative.
|
|
110
|
+
def self.role_for(path, root: nil)
|
|
111
|
+
path = relative_to(path, root)
|
|
65
112
|
case path
|
|
66
|
-
when %r{(\A|/)(spec|test)/}, /_(spec|test)\.rb\z/ then :test
|
|
113
|
+
when %r{(\A|/)(spec|test)/}, /_(spec|test)\.rb\z/, %r{\A(\./)?(qa|e2e|features)/} then :test
|
|
67
114
|
when /\.rake\z/, %r{(\A|/)(lib/)?tasks/} then :task
|
|
68
115
|
when %r{(\A|/)config/} then :config
|
|
69
116
|
else :production
|
|
70
117
|
end
|
|
71
118
|
end
|
|
72
119
|
|
|
120
|
+
def self.relative_to(path, root)
|
|
121
|
+
return path if root.nil?
|
|
122
|
+
|
|
123
|
+
prefix = "#{root.to_s.chomp('/')}/"
|
|
124
|
+
path.start_with?(prefix) ? path.delete_prefix(prefix) : path
|
|
125
|
+
end
|
|
126
|
+
private_class_method :relative_to
|
|
127
|
+
|
|
73
128
|
# A constant name is ASCII by construction, so a byte sequence that is not valid UTF-8 cannot be one.
|
|
74
129
|
# Dropping it is both correct and the only safe answer: carrying it forward crashed the whole run on
|
|
75
130
|
# the first `String#sub` downstream, which is how this surfaced — `rigor unused` on Rigor's own
|
|
@@ -84,9 +139,11 @@ module Rigor
|
|
|
84
139
|
# @param path — the file's path, as the report should render it.
|
|
85
140
|
# @param source — the file's bytes.
|
|
86
141
|
# @param target_ruby — Prism version string, threaded from the project configuration.
|
|
142
|
+
# @param root — the project root `path` is made relative to when its role is decided.
|
|
87
143
|
# @return nil when the file does not parse (a parse error is the analyzer's business, not
|
|
88
144
|
# this scan's — it simply contributes nothing rather than half a file).
|
|
89
|
-
|
|
145
|
+
# `role`, when given, is the referring file's role, decided by the caller instead of from `path` (#1760).
|
|
146
|
+
def self.call(path:, source:, target_ruby: nil, root: nil, role: nil)
|
|
90
147
|
parsed = if target_ruby
|
|
91
148
|
Prism.parse(source, filepath: path,
|
|
92
149
|
version: target_ruby)
|
|
@@ -95,14 +152,314 @@ module Rigor
|
|
|
95
152
|
end
|
|
96
153
|
return nil unless parsed.success?
|
|
97
154
|
|
|
98
|
-
walker = Walker.new(path: path, role: role_for(path))
|
|
155
|
+
walker = Walker.new(path: path, role: role || role_for(path, root: root))
|
|
99
156
|
walker.walk(parsed.value, [])
|
|
100
157
|
Result.new(declarations: walker.declarations.freeze, references: walker.references.freeze,
|
|
101
158
|
dynamic_uses: walker.dynamic_uses.freeze)
|
|
102
159
|
end
|
|
103
160
|
|
|
161
|
+
# The values a local variable can hold, read off its assignments in one local scope, for a
|
|
162
|
+
# dynamic-resolution argument built a line before the call (#1734).
|
|
163
|
+
module LocalValues
|
|
164
|
+
module_function
|
|
165
|
+
|
|
166
|
+
# The assigned value nodes of `name` in `scope`, or nil when there is none or one of the assignments
|
|
167
|
+
# cannot be read off its node (`+=`, a multiple assignment, `rescue => name`).
|
|
168
|
+
def values(scope, name)
|
|
169
|
+
writes = []
|
|
170
|
+
collect_local_writes(scope, name, writes)
|
|
171
|
+
return nil if writes.empty? || writes.any? { |write| !VALUE_WRITES.include?(write.class) }
|
|
172
|
+
|
|
173
|
+
writes.map(&:value)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The names a block's parameter list binds, block-locals included.
|
|
177
|
+
def bindings(parameters)
|
|
178
|
+
out = []
|
|
179
|
+
collect_block_bindings(parameters, out)
|
|
180
|
+
out
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
VALUE_WRITES = [Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode,
|
|
184
|
+
Prism::LocalVariableAndWriteNode].freeze
|
|
185
|
+
private_constant :VALUE_WRITES
|
|
186
|
+
|
|
187
|
+
LOCAL_WRITES = (VALUE_WRITES + [Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableTargetNode]).freeze
|
|
188
|
+
private_constant :LOCAL_WRITES
|
|
189
|
+
|
|
190
|
+
# Nodes that open a new local-variable scope. A block does not: it shares its method's locals.
|
|
191
|
+
SCOPE_GATES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
192
|
+
private_constant :SCOPE_GATES
|
|
193
|
+
|
|
194
|
+
def collect_local_writes(node, name, out)
|
|
195
|
+
node.rigor_each_child do |child|
|
|
196
|
+
next if SCOPE_GATES.any? { |gate| child.is_a?(gate) }
|
|
197
|
+
|
|
198
|
+
out << child if LOCAL_WRITES.any? { |kind| child.is_a?(kind) } && child.name == name
|
|
199
|
+
collect_local_writes(child, name, out)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
BLOCK_BINDINGS = [Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
|
|
204
|
+
Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
|
|
205
|
+
Prism::KeywordRestParameterNode, Prism::BlockParameterNode,
|
|
206
|
+
Prism::BlockLocalVariableNode].freeze
|
|
207
|
+
private_constant :BLOCK_BINDINGS
|
|
208
|
+
|
|
209
|
+
def collect_block_bindings(node, out)
|
|
210
|
+
out << node.name if BLOCK_BINDINGS.any? { |kind| node.is_a?(kind) } && node.name
|
|
211
|
+
node.rigor_each_child { |child| collect_block_bindings(child, out) }
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# `Const = Class.new` / `Module.new` / `Data.define(...)` / `Struct.new(...)` declare a class under a
|
|
216
|
+
# constant, and `ScopeIndexer#record_class_new_constant_decl` already treats them as class declarations
|
|
217
|
+
# for cross-file resolution. The report must agree, or every such constant reads as an unreferenced
|
|
218
|
+
# value rather than a class.
|
|
219
|
+
META_NEW = { "Class" => :new, "Module" => :new, "Data" => :define, "Struct" => :new }.freeze
|
|
220
|
+
private_constant :META_NEW
|
|
221
|
+
|
|
222
|
+
# ADR-102 WD4 — the {Walker}'s reading of dynamic constant resolution: `constantize` and `const_get`
|
|
223
|
+
# sites, the receivers a `const_get` looks its name up from, and the values its argument can hold.
|
|
224
|
+
module DynamicSites
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
# Names that turn a String into a constant. `constantize` / `safe_constantize` are ActiveSupport;
|
|
228
|
+
# `const_get` is core and may carry an explicit receiver (`Object.const_get`, `self.class.const_get`).
|
|
229
|
+
DYNAMIC_RESOLVERS = %i[constantize safe_constantize const_get].freeze
|
|
230
|
+
private_constant :DYNAMIC_RESOLVERS
|
|
231
|
+
|
|
232
|
+
# The anchor of a lookup that starts at the top level.
|
|
233
|
+
TOP_LEVEL = ""
|
|
234
|
+
private_constant :TOP_LEVEL
|
|
235
|
+
|
|
236
|
+
SUBJECT_SHAPES = {
|
|
237
|
+
Prism::StringNode => "a literal string",
|
|
238
|
+
Prism::SymbolNode => "a literal symbol",
|
|
239
|
+
Prism::InterpolatedStringNode => "an interpolated string",
|
|
240
|
+
Prism::InterpolatedSymbolNode => "an interpolated symbol"
|
|
241
|
+
}.freeze
|
|
242
|
+
private_constant :SUBJECT_SHAPES
|
|
243
|
+
|
|
244
|
+
# Calls that hand a string or symbol through unchanged, as far as the name it spells: `"V#{v}".freeze`.
|
|
245
|
+
PASS_THROUGH = %i[freeze to_s to_str to_sym dup -@ +@].freeze
|
|
246
|
+
private_constant :PASS_THROUGH
|
|
247
|
+
|
|
248
|
+
# Blocks whose `self` is not the enclosing declaration: a concern's `included` / `class_methods` body
|
|
249
|
+
# runs on the including class, and an eval block on its receiver.
|
|
250
|
+
SELF_REBINDING = %i[included prepended extended class_methods class_eval module_eval class_exec
|
|
251
|
+
module_exec instance_eval instance_exec].freeze
|
|
252
|
+
private_constant :SELF_REBINDING
|
|
253
|
+
|
|
254
|
+
# A call is a dynamic-resolution site, and its block may run with another `self` ({#rebinds_self?}).
|
|
255
|
+
def record_call(node, nesting)
|
|
256
|
+
record_dynamic_use(node, nesting)
|
|
257
|
+
@rebinding_blocks << node.block if node.block.is_a?(Prism::BlockNode) && rebinds_self?(node)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
|
|
261
|
+
# as a blanket namespace poison: a literal argument names the exact constant and is as good as a
|
|
262
|
+
# written reference, while an interpolated one can only bound the names its literal head reaches.
|
|
263
|
+
#
|
|
264
|
+
# A local variable argument is read through its assignments in the same local scope, because the
|
|
265
|
+
# name is usually built one line up: GitLab's `Migration[2.2]` is `name = "V#{...}"` followed by
|
|
266
|
+
# `const_get(name, false)` (#1734).
|
|
267
|
+
def record_dynamic_use(node, nesting)
|
|
268
|
+
return unless DYNAMIC_RESOLVERS.include?(node.name)
|
|
269
|
+
|
|
270
|
+
subject = node.name == :const_get ? node.arguments&.arguments&.first : node.receiver
|
|
271
|
+
return if subject.nil?
|
|
272
|
+
|
|
273
|
+
anchors = node.name == :const_get ? const_get_anchors(node, nesting) : [TOP_LEVEL]
|
|
274
|
+
subjects(subject).each { |value| record_dynamic_subject(node, value, anchors) }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def record_dynamic_subject(node, subject, anchors)
|
|
278
|
+
name, head = dynamic_target(subject)
|
|
279
|
+
# A literal whose bytes are not valid UTF-8 cannot name a constant; dropping it is the only safe
|
|
280
|
+
# answer, and carrying it forward crashed the whole run downstream.
|
|
281
|
+
return if subject.is_a?(Prism::StringNode) && name.nil?
|
|
282
|
+
return if subject.is_a?(Prism::SymbolNode) && name.nil?
|
|
283
|
+
|
|
284
|
+
reason = "#{node.name} on #{SUBJECT_SHAPES.fetch(subject.class, 'a computed value')}"
|
|
285
|
+
bounds = if name then literal_bounds(node, name, anchors)
|
|
286
|
+
elsif head then anchors.filter_map { |anchor| bound(head, anchor) }.uniq
|
|
287
|
+
else []
|
|
288
|
+
end
|
|
289
|
+
bounds = [[nil, :namespace, nil]] if bounds.empty?
|
|
290
|
+
bounds.each do |prefix, scope, within|
|
|
291
|
+
@dynamic_uses << DynamicUse.new(name: name, prefix: prefix, scope: scope, within: within,
|
|
292
|
+
inherit: anchors.include?(TOP_LEVEL), role: @role,
|
|
293
|
+
site: site(node), path: @path, line: node.location.start_line,
|
|
294
|
+
reason: reason)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# A literal name is looked up from each receiver the call can have, so it carries the receiver as
|
|
299
|
+
# `within` and {Graph} resolves it there first (#1761). A rooted name, or a call whose only anchor is
|
|
300
|
+
# the top level, is looked up from the top level as any reference is.
|
|
301
|
+
#
|
|
302
|
+
# On `self` the receiver is the enclosing declaration only where the walk can name it ({#self_lookup}).
|
|
303
|
+
# Where `self` is a `Class` or a singleton class, Ruby looks the name up at the top level. Where the
|
|
304
|
+
# walk cannot tell what `self` is, the name is read BOTH at the enclosing declaration and at the top
|
|
305
|
+
# level: either reading alone lists a live constant as a candidate when it is the wrong one, and a
|
|
306
|
+
# dead constant kept reachable is the cheaper mistake. An interpolated name keeps every anchor, since
|
|
307
|
+
# a taint is safe to over-reach.
|
|
308
|
+
def literal_bounds(node, name, anchors)
|
|
309
|
+
return [] if name.start_with?("::")
|
|
310
|
+
|
|
311
|
+
within = anchors.grep(Reference).map { |receiver| [nil, :namespace, receiver] }
|
|
312
|
+
return within unless self_receiver?(node.receiver)
|
|
313
|
+
|
|
314
|
+
case self_lookup(node.receiver)
|
|
315
|
+
when :declaration then within
|
|
316
|
+
when :top_level then []
|
|
317
|
+
else within + [[nil, :namespace, nil]]
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Where a `const_get` on `self` (implicit, `self`, or `self.class`) looks its name up: `:declaration`
|
|
322
|
+
# when `self` is the enclosing class or module, `:top_level` when it is `Class` or a singleton class,
|
|
323
|
+
# `:unknown` otherwise. A block's `self` is unknown — `included do`, `class_eval` and an instance-
|
|
324
|
+
# evaluated DSL block (`configure { }`, an RSpec `describe` body) all rebind it, and the walk cannot
|
|
325
|
+
# tell those from an `each` block. So is the `self` of a `def Other.x` or of a `class << Other` body.
|
|
326
|
+
def self_lookup(receiver)
|
|
327
|
+
return :unknown if @in_block || @self_rebound
|
|
328
|
+
|
|
329
|
+
class_of_self = receiver.is_a?(Prism::CallNode)
|
|
330
|
+
case @self_kind
|
|
331
|
+
when :declaration, :singleton_method then class_of_self ? :top_level : :declaration
|
|
332
|
+
when :instance_method then class_of_self ? :declaration : :unknown
|
|
333
|
+
when :singleton_class then :top_level
|
|
334
|
+
else :unknown
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Where a `const_get` looks a name up, as the namespaces its interpolated argument is bounded by. The
|
|
339
|
+
# receiver's namespace comes first: an implicit receiver, `self` or `self.class` is the enclosing
|
|
340
|
+
# declaration, and a constant receiver is resolved by {Graph} as any reference is (`within`). With
|
|
341
|
+
# the default `inherit = true` the lookup also falls through to the top level, which is all the
|
|
342
|
+
# reading had before, so that stays tainted too; an explicit `false` confines it to the receiver. An
|
|
343
|
+
# unknown receiver bounds nothing beyond the top level.
|
|
344
|
+
def const_get_anchors(node, nesting)
|
|
345
|
+
receiver = node.receiver
|
|
346
|
+
namespaces =
|
|
347
|
+
if self_receiver?(receiver)
|
|
348
|
+
self_references(node, nesting)
|
|
349
|
+
elsif constant_receiver?(receiver)
|
|
350
|
+
[receiver_reference(receiver, nesting)]
|
|
351
|
+
else
|
|
352
|
+
[]
|
|
353
|
+
end
|
|
354
|
+
return [TOP_LEVEL] if namespaces.empty?
|
|
355
|
+
|
|
356
|
+
inherit = node.arguments.arguments[1]
|
|
357
|
+
inherit.is_a?(Prism::FalseNode) ? namespaces : namespaces + [TOP_LEVEL]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# The enclosing declaration as a rooted {Reference}, so {Graph} anchors it as it anchors a constant
|
|
361
|
+
# receiver and can search its ancestors. Inside a meta-new rvalue `self` is the declared class in its
|
|
362
|
+
# block but the outer scope in its arguments; the walk does not tell the two apart, so both are taken.
|
|
363
|
+
def self_references(node, nesting)
|
|
364
|
+
([@owner, nesting.join("::")].compact.uniq - [TOP_LEVEL]).map do |fqn|
|
|
365
|
+
Reference.new(as_written: fqn, nesting: [].freeze, from: nil, role: @role, path: @path,
|
|
366
|
+
line: node.location.start_line, rooted: true)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Whether `call`'s block runs with a `self` other than the enclosing declaration's. A meta-new block
|
|
371
|
+
# assigned to a constant is that constant ({#walk_constant_write} credits it as `@owner`); an
|
|
372
|
+
# anonymous one is not the enclosing declaration either.
|
|
373
|
+
def rebinds_self?(call)
|
|
374
|
+
return true if SELF_REBINDING.include?(call.name)
|
|
375
|
+
|
|
376
|
+
recv = call.receiver
|
|
377
|
+
name = recv && Source::ConstantPath.qualified_name_or_nil(recv)&.delete_prefix("::")
|
|
378
|
+
@owner.nil? && !name.nil? && META_NEW[name] == call.name
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def self_receiver?(receiver)
|
|
382
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode) ||
|
|
383
|
+
(receiver.is_a?(Prism::CallNode) && receiver.name == :class && receiver.receiver.is_a?(Prism::SelfNode))
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# `Object.const_get` is the top level, which every lookup already reaches.
|
|
387
|
+
def constant_receiver?(receiver)
|
|
388
|
+
return false unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
389
|
+
|
|
390
|
+
as_written = Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
391
|
+
!as_written.nil? && as_written.delete_prefix("::") != "Object"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def receiver_reference(receiver, nesting)
|
|
395
|
+
Reference.new(as_written: Source::ConstantPath.qualified_name_or_nil(receiver),
|
|
396
|
+
nesting: nesting.dup.freeze, from: nil, role: @role, path: @path,
|
|
397
|
+
line: receiver.location.start_line, rooted: Source::ConstantPath.rooted?(receiver))
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# `[prefix, scope, within]` for a literal head under one anchor. A head ending in `::` names a
|
|
401
|
+
# namespace and reaches everything beneath it; any other head is the start of a name, so
|
|
402
|
+
# `"V#{version}"` reaches `V1_0` and `V2_2` alike, and only a string-prefix match says that. A rooted
|
|
403
|
+
# head ignores the receiver. `within` carries a constant receiver for {Graph} to resolve.
|
|
404
|
+
def bound(head, anchor)
|
|
405
|
+
anchor = TOP_LEVEL if head.start_with?("::")
|
|
406
|
+
head = head.delete_prefix("::")
|
|
407
|
+
scope = head.end_with?("::") ? :namespace : :leading
|
|
408
|
+
head = head.delete_suffix("::")
|
|
409
|
+
return nil if head.empty?
|
|
410
|
+
|
|
411
|
+
anchor == TOP_LEVEL ? [head, scope, nil] : [head, scope, anchor]
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# The values a dynamic-resolution subject can hold: the subject itself, or for a local variable every
|
|
415
|
+
# value assigned to it in the enclosing local scope. A local with an assignment whose value cannot be
|
|
416
|
+
# read off the node (`+=`, a multiple assignment, `rescue => name`) stays a computed value.
|
|
417
|
+
#
|
|
418
|
+
# A name an enclosing block binds as a parameter or block-local (`|name|`, `|x; name|`) is that
|
|
419
|
+
# block's value, not the method's: reading it through the method's assignments named a literal the
|
|
420
|
+
# block never sees, and a constant only that literal named was hidden from the report.
|
|
421
|
+
def subjects(subject)
|
|
422
|
+
subject = pass_through(subject)
|
|
423
|
+
return [subject] unless subject.is_a?(Prism::LocalVariableReadNode) && @local_scope
|
|
424
|
+
return [subject] if @block_params.include?(subject.name)
|
|
425
|
+
|
|
426
|
+
(LocalValues.values(@local_scope, subject.name) || [subject]).map { |value| pass_through(value) }
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# The string or symbol under `"V#{v}".freeze` / `.to_sym`; any other node unchanged.
|
|
430
|
+
def pass_through(node)
|
|
431
|
+
while node.is_a?(Prism::CallNode) && PASS_THROUGH.include?(node.name) && node.arguments.nil? &&
|
|
432
|
+
node.block.nil? && SUBJECT_SHAPES.key?(pass_through(node.receiver).class)
|
|
433
|
+
node = node.receiver
|
|
434
|
+
end
|
|
435
|
+
node
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# `[exact name, literal head]` for a dynamic-resolution subject. A literal names its constant exactly;
|
|
439
|
+
# an interpolation can only bound the names its literal head starts; anything else bounds nothing.
|
|
440
|
+
def dynamic_target(subject)
|
|
441
|
+
case subject
|
|
442
|
+
when Prism::StringNode, Prism::SymbolNode then [Scan.usable_name(subject.unescaped), nil]
|
|
443
|
+
when Prism::InterpolatedStringNode, Prism::InterpolatedSymbolNode then [nil, literal_head(subject)]
|
|
444
|
+
else [nil, nil]
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# The literal head of an interpolated name, as written: `"Foo::Bar::#{k}"` gives `Foo::Bar::`. Returns
|
|
449
|
+
# nil when the interpolation starts the string, which bounds nothing.
|
|
450
|
+
def literal_head(node)
|
|
451
|
+
head = node.parts.first
|
|
452
|
+
return nil unless head.is_a?(Prism::StringNode)
|
|
453
|
+
|
|
454
|
+
literal = Scan.usable_name(head.unescaped)
|
|
455
|
+
literal.nil? || literal.empty? ? nil : literal
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
104
459
|
# Single-pass walker. Tracks `nesting` as the stack of enclosing declaration names.
|
|
105
460
|
class Walker
|
|
461
|
+
include DynamicSites
|
|
462
|
+
|
|
106
463
|
attr_reader :declarations, :references, :dynamic_uses
|
|
107
464
|
|
|
108
465
|
def initialize(path:, role:)
|
|
@@ -111,6 +468,25 @@ module Rigor
|
|
|
111
468
|
@declarations = []
|
|
112
469
|
@references = []
|
|
113
470
|
@dynamic_uses = []
|
|
471
|
+
# The declaration a meta-new rvalue is being walked for, or nil. See {#walk_constant_write}.
|
|
472
|
+
@owner = nil
|
|
473
|
+
# The node whose subtree holds the local variables in scope: the file, a method, or a class body.
|
|
474
|
+
@local_scope = nil
|
|
475
|
+
# Names an enclosing block's parameters or block-locals shadow within @local_scope.
|
|
476
|
+
@block_params = Set.new.freeze
|
|
477
|
+
# Whether `self` here is something other than the enclosing declaration — inside an `included do`,
|
|
478
|
+
# `class_eval` or anonymous `Class.new` block. See {#rebinds_self?}.
|
|
479
|
+
@self_rebound = false
|
|
480
|
+
@rebinding_blocks = Set.new.compare_by_identity
|
|
481
|
+
# What `self` is here, for {#self_lookup}: `:declaration` in a class or module body (and at the top
|
|
482
|
+
# level), `:singleton_method` in a `def self.x` or a def in a `class << self` body,
|
|
483
|
+
# `:instance_method` in any other def, `:singleton_class` in a `class << self` body outside its
|
|
484
|
+
# methods, and `:unknown` where the walk cannot name it.
|
|
485
|
+
@self_kind = :declaration
|
|
486
|
+
# Whether the walk is inside a block, whose `self` it cannot name. See {#self_lookup}.
|
|
487
|
+
@in_block = false
|
|
488
|
+
# Whether the innermost declaration is a module, whose instance methods' `self.class` is not it.
|
|
489
|
+
@module_body = false
|
|
114
490
|
end
|
|
115
491
|
|
|
116
492
|
def walk(node, nesting)
|
|
@@ -118,19 +494,27 @@ module Rigor
|
|
|
118
494
|
|
|
119
495
|
case node
|
|
120
496
|
when Prism::ClassNode then return walk_declaration(node, nesting, superclass: node.superclass)
|
|
121
|
-
when Prism::ModuleNode then return walk_declaration(node, nesting, superclass: nil)
|
|
497
|
+
when Prism::ModuleNode then return walk_declaration(node, nesting, superclass: nil, module_body: true)
|
|
122
498
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
123
499
|
record_reference(node, nesting)
|
|
124
500
|
# A constant path's segments are not separate references — `A::B::C` is one reference to the leaf,
|
|
125
501
|
# and descending would record `A` and `A::B` as references in their own right (22 spurious
|
|
126
502
|
# candidates on Rigor's own lib came from exactly that in the #345 probe).
|
|
503
|
+
#
|
|
504
|
+
# A path on a computed base is the exception: `Migration[2.2]::MigrationRecord` names no constant
|
|
505
|
+
# this reading can resolve, but its base is a call whose receiver and arguments do.
|
|
506
|
+
walk(node.parent, nesting) if node.is_a?(Prism::ConstantPathNode) &&
|
|
507
|
+
Source::ConstantPath.qualified_name_or_nil(node).nil?
|
|
127
508
|
return
|
|
128
509
|
when Prism::ConstantWriteNode
|
|
129
|
-
|
|
130
|
-
walk(node.value, nesting)
|
|
510
|
+
walk_constant_write(node, nesting)
|
|
131
511
|
return
|
|
132
512
|
when Prism::CallNode
|
|
133
|
-
|
|
513
|
+
record_call(node, nesting)
|
|
514
|
+
when Prism::ProgramNode, Prism::DefNode, Prism::SingletonClassNode
|
|
515
|
+
return in_local_scope(node) { node.rigor_each_child { |child| walk(child, nesting) } }
|
|
516
|
+
when Prism::BlockNode, Prism::LambdaNode
|
|
517
|
+
return in_block(node) { node.rigor_each_child { |child| walk(child, nesting) } }
|
|
134
518
|
end
|
|
135
519
|
|
|
136
520
|
node.rigor_each_child { |child| walk(child, nesting) }
|
|
@@ -138,28 +522,59 @@ module Rigor
|
|
|
138
522
|
|
|
139
523
|
private
|
|
140
524
|
|
|
141
|
-
def walk_declaration(node, nesting, superclass:)
|
|
525
|
+
def walk_declaration(node, nesting, superclass:, module_body: false)
|
|
142
526
|
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
143
527
|
return node.rigor_each_child { |child| walk(child, nesting) } if name.nil?
|
|
144
528
|
|
|
145
529
|
fqn = (nesting + [name]).join("::")
|
|
146
530
|
# The superclass position IS a reference — `class Sub < Base` reads `Base` — and it resolves against
|
|
147
|
-
# the OUTER nesting, not inside the body being opened.
|
|
148
|
-
|
|
531
|
+
# the OUTER nesting, not inside the body being opened. It is part of `Sub`'s declaration, though, so
|
|
532
|
+
# it is credited to `Sub` (#1720): crediting the enclosing scope left a base nested in a module that
|
|
533
|
+
# is never itself reached unreachable, and rooted a top-level base even when every subclass is dead.
|
|
534
|
+
#
|
|
535
|
+
# The superclass is WALKED, not only read as a name: `< DelegateClass(Foo)`, `< Struct.new(:a,
|
|
536
|
+
# Foo::X)` and `< ActiveRecord::Migration[7.1]` are calls whose receiver and arguments name
|
|
537
|
+
# constants, and reading only a constant node recorded none of them, so `Foo` was a false candidate
|
|
538
|
+
# (#1733). Its references take the credit a meta-new rvalue's do.
|
|
539
|
+
walk_owned(superclass, nesting, fqn) if superclass
|
|
149
540
|
includes = node.body ? mixin_names(node.body) : []
|
|
150
541
|
@declarations << Declaration.new(fqn: fqn, path: @path, line: node.location.start_line,
|
|
151
542
|
superclass: superclass && Source::ConstantPath.qualified_name(superclass),
|
|
152
543
|
includes: includes.freeze)
|
|
153
|
-
|
|
544
|
+
return unless node.body
|
|
545
|
+
|
|
546
|
+
# The body's nesting names the declaration itself, so a credit inherited from a meta-new rvalue
|
|
547
|
+
# this declaration sits in must not leak into it.
|
|
548
|
+
owner = @owner
|
|
549
|
+
saved = [@self_rebound, @in_block, @module_body]
|
|
550
|
+
@owner = nil
|
|
551
|
+
@self_rebound = @in_block = false
|
|
552
|
+
@module_body = module_body
|
|
553
|
+
in_local_scope(node.body, :declaration) { walk(node.body, nesting + [name]) }
|
|
554
|
+
@owner = owner
|
|
555
|
+
@self_rebound, @in_block, @module_body = saved
|
|
154
556
|
end
|
|
155
557
|
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
558
|
+
# A meta-new rvalue — `Class.new(Base) { ... }`, `Struct.new(:a) do ... end` — is the declaration's
|
|
559
|
+
# header and body, so the references in it are credited to the declared constant, exactly as a
|
|
560
|
+
# `class Sub < Base` superclass is (#1720). Its lexical nesting is unchanged: a block opens no cref.
|
|
561
|
+
def walk_constant_write(node, nesting)
|
|
562
|
+
fqn = record_meta_new(node, nesting)
|
|
563
|
+
return walk(node.value, nesting) if fqn.nil?
|
|
564
|
+
|
|
565
|
+
walk_owned(node.value, nesting, fqn)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Walks a declaration's header expression in the outer `nesting` it is evaluated in, crediting the
|
|
569
|
+
# references in it to the declared `fqn`.
|
|
570
|
+
def walk_owned(node, nesting, fqn)
|
|
571
|
+
owner = @owner
|
|
572
|
+
@owner = fqn
|
|
573
|
+
walk(node, nesting)
|
|
574
|
+
@owner = owner
|
|
575
|
+
end
|
|
162
576
|
|
|
577
|
+
# @return the declared FQN, or nil when the rvalue is not a meta-new form.
|
|
163
578
|
def record_meta_new(node, nesting)
|
|
164
579
|
call = node.value
|
|
165
580
|
return unless call.is_a?(Prism::CallNode)
|
|
@@ -167,8 +582,10 @@ module Rigor
|
|
|
167
582
|
recv = call.receiver
|
|
168
583
|
return unless recv.is_a?(Prism::ConstantReadNode) && META_NEW[recv.name.to_s] == call.name
|
|
169
584
|
|
|
170
|
-
|
|
585
|
+
fqn = (nesting + [node.name.to_s]).join("::")
|
|
586
|
+
@declarations << Declaration.new(fqn: fqn, path: @path,
|
|
171
587
|
line: node.location.start_line, superclass: nil, includes: [].freeze)
|
|
588
|
+
fqn
|
|
172
589
|
end
|
|
173
590
|
|
|
174
591
|
# `include` / `prepend` / `extend` argument names written directly in the declaration body. Only the
|
|
@@ -184,73 +601,69 @@ module Rigor
|
|
|
184
601
|
end
|
|
185
602
|
end
|
|
186
603
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
Prism::InterpolatedStringNode => "an interpolated string"
|
|
196
|
-
}.freeze
|
|
197
|
-
private_constant :SUBJECT_SHAPES
|
|
198
|
-
|
|
199
|
-
# Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
|
|
200
|
-
# as a blanket namespace poison: a literal argument names the exact constant and is as good as a
|
|
201
|
-
# written reference, while an interpolated one can only bound the namespace it reaches into.
|
|
202
|
-
def record_dynamic_use(node)
|
|
203
|
-
return unless DYNAMIC_RESOLVERS.include?(node.name)
|
|
204
|
-
|
|
205
|
-
subject = node.name == :const_get ? node.arguments&.arguments&.first : node.receiver
|
|
206
|
-
return if subject.nil?
|
|
207
|
-
|
|
208
|
-
name, prefix = dynamic_target(subject)
|
|
209
|
-
# A literal whose bytes are not valid UTF-8 cannot name a constant; dropping it is the only safe
|
|
210
|
-
# answer, and carrying it forward crashed the whole run downstream.
|
|
211
|
-
return if subject.is_a?(Prism::StringNode) && name.nil?
|
|
212
|
-
return if subject.is_a?(Prism::SymbolNode) && name.nil?
|
|
213
|
-
|
|
214
|
-
@dynamic_uses << DynamicUse.new(
|
|
215
|
-
name: name, prefix: prefix, site: site(node), path: @path, line: node.location.start_line,
|
|
216
|
-
reason: "#{node.name} on #{SUBJECT_SHAPES.fetch(subject.class, 'a computed value')}"
|
|
217
|
-
)
|
|
604
|
+
def in_local_scope(scope, self_kind = self_kind_in(scope))
|
|
605
|
+
saved = [@local_scope, @block_params, @self_kind]
|
|
606
|
+
@local_scope = scope
|
|
607
|
+
@block_params = Set.new.freeze
|
|
608
|
+
@self_kind = self_kind
|
|
609
|
+
yield
|
|
610
|
+
ensure
|
|
611
|
+
@local_scope, @block_params, @self_kind = saved
|
|
218
612
|
end
|
|
219
613
|
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
when Prism::
|
|
226
|
-
|
|
227
|
-
|
|
614
|
+
# The {#self_lookup} kind inside `scope`, from the kind around it. Only a receiver-less def or a `def
|
|
615
|
+
# self.x` directly in a declaration body, and a `class << self` there, keep `self` nameable.
|
|
616
|
+
def self_kind_in(scope)
|
|
617
|
+
case scope
|
|
618
|
+
when Prism::ProgramNode then :declaration
|
|
619
|
+
when Prism::SingletonClassNode
|
|
620
|
+
scope.expression.is_a?(Prism::SelfNode) && @self_kind == :declaration ? :singleton_class : :unknown
|
|
621
|
+
when Prism::DefNode then def_self_kind(scope)
|
|
622
|
+
else :unknown
|
|
228
623
|
end
|
|
229
624
|
end
|
|
230
625
|
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
def
|
|
234
|
-
|
|
626
|
+
# A receiver-less def names its `self` only directly in a class body or a `class << self` body. In a
|
|
627
|
+
# module body `self.class` is the includer's class, or `Module` under `module_function` / `extend
|
|
628
|
+
# self`; a def nested in another def lands on whatever that method's cref is, which the walk cannot
|
|
629
|
+
# tell apart.
|
|
630
|
+
def def_self_kind(node)
|
|
631
|
+
if node.receiver.nil?
|
|
632
|
+
case @self_kind
|
|
633
|
+
when :singleton_class then :singleton_method
|
|
634
|
+
when :declaration then @module_body ? :unknown : :instance_method
|
|
635
|
+
else :unknown
|
|
636
|
+
end
|
|
637
|
+
elsif node.receiver.is_a?(Prism::SelfNode) && @self_kind == :declaration
|
|
638
|
+
:singleton_method
|
|
639
|
+
else
|
|
640
|
+
:unknown
|
|
641
|
+
end
|
|
235
642
|
end
|
|
236
643
|
|
|
237
|
-
def
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
644
|
+
def in_block(block)
|
|
645
|
+
saved = [@block_params, @self_rebound, @in_block]
|
|
646
|
+
names = block.parameters ? LocalValues.bindings(block.parameters) : []
|
|
647
|
+
@block_params = (@block_params | names).freeze unless names.empty?
|
|
648
|
+
@self_rebound ||= @rebinding_blocks.include?(block)
|
|
649
|
+
@in_block = true
|
|
650
|
+
yield
|
|
651
|
+
ensure
|
|
652
|
+
@block_params, @self_rebound, @in_block = saved
|
|
653
|
+
end
|
|
243
654
|
|
|
244
|
-
|
|
245
|
-
|
|
655
|
+
def site(node)
|
|
656
|
+
"#{@path}:#{node.location.start_line}"
|
|
246
657
|
end
|
|
247
658
|
|
|
248
|
-
|
|
659
|
+
# `from` defaults to the innermost enclosing declaration; a declaration's own header (its superclass,
|
|
660
|
+
# its meta-new rvalue) is credited to that declaration instead.
|
|
661
|
+
def record_reference(node, nesting, from: @owner)
|
|
249
662
|
as_written = Source::ConstantPath.qualified_name_or_nil(node)
|
|
250
663
|
return if as_written.nil?
|
|
251
664
|
|
|
252
|
-
|
|
253
|
-
|
|
665
|
+
from ||= nesting.empty? ? nil : nesting.join("::")
|
|
666
|
+
@references << Reference.new(as_written: as_written, nesting: nesting.dup.freeze, from: from,
|
|
254
667
|
role: @role, path: @path, line: node.location.start_line,
|
|
255
668
|
rooted: Source::ConstantPath.rooted?(node))
|
|
256
669
|
end
|