rigortype 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -391,6 +391,21 @@ module Rigor
|
|
|
391
391
|
end
|
|
392
392
|
private_class_method :plain_constant_path?
|
|
393
393
|
|
|
394
|
+
# {read_name} for prebuilt, frozen `"kind:name"` keys: a caller that files the same keys on every read
|
|
395
|
+
# (a resolution chain's hook edges, #1641) builds them once and adds them here without a String apiece.
|
|
396
|
+
def read_keys(keys)
|
|
397
|
+
accumulator = Thread.current[KEY]
|
|
398
|
+
return if accumulator.nil?
|
|
399
|
+
|
|
400
|
+
captures = Thread.current[CAPTURE_KEY]
|
|
401
|
+
open = captures && !captures.empty?
|
|
402
|
+
missing = accumulator.missing
|
|
403
|
+
keys.each do |entry|
|
|
404
|
+
tee_missing(captures, entry) if open
|
|
405
|
+
missing.add(entry)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
394
409
|
# The miss-only spelling of {read_name}: a cross-file lookup of `name` (kind `:method` / `:class` /
|
|
395
410
|
# `:toplevel` / …) that resolved to nothing. Kept as the name the internal spec's negative-edge
|
|
396
411
|
# contract uses.
|
|
@@ -42,6 +42,9 @@ module Rigor
|
|
|
42
42
|
@class_to_gem = {}
|
|
43
43
|
@budget_exceeded = []
|
|
44
44
|
@gem_modes = {}
|
|
45
|
+
@refinements = {}
|
|
46
|
+
@bound_constants = Set.new
|
|
47
|
+
@bound_segments = Set.new
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
def absorb(outcome, budget, builder)
|
|
@@ -58,6 +61,9 @@ module Rigor
|
|
|
58
61
|
@gem_modes[resolved.gem_name] = resolved.mode
|
|
59
62
|
walked = builder.walker_outcome_for(resolved, budget)
|
|
60
63
|
@catalog.merge!(walked.catalog)
|
|
64
|
+
@refinements = Inference::ScopeIndexer.merge_refinement_tables(@refinements, walked.refinements)
|
|
65
|
+
@bound_constants.merge(walked.bound)
|
|
66
|
+
@bound_segments.merge(walked.bound_segments)
|
|
61
67
|
builder.record_class_to_gem(walked.catalog, resolved.gem_name, @class_to_gem)
|
|
62
68
|
@budget_exceeded << resolved.gem_name if walked.truncated?
|
|
63
69
|
end
|
|
@@ -68,7 +74,8 @@ module Rigor
|
|
|
68
74
|
method_catalog: @catalog, budget_exceeded: @budget_exceeded,
|
|
69
75
|
class_to_gem: @class_to_gem,
|
|
70
76
|
budget_overrun_strategy: dependencies.budget_overrun_strategy,
|
|
71
|
-
gem_modes: @gem_modes
|
|
77
|
+
gem_modes: @gem_modes, refinements: @refinements, bound_constants: @bound_constants,
|
|
78
|
+
bound_segments: @bound_segments
|
|
72
79
|
)
|
|
73
80
|
end
|
|
74
81
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../cache/descriptor"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Analysis
|
|
5
7
|
module DependencySourceInference
|
|
@@ -19,6 +21,11 @@ module Rigor
|
|
|
19
21
|
def descriptor_key
|
|
20
22
|
[gem_name, version, mode].freeze
|
|
21
23
|
end
|
|
24
|
+
|
|
25
|
+
# ADR-10 slice 3 — this gem's row of a cache descriptor: its name, resolved version and mode.
|
|
26
|
+
def dependency_entry
|
|
27
|
+
Cache::Descriptor::DependencyEntry.new(gem_name: gem_name, gem_version: version, mode: mode)
|
|
28
|
+
end
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
# Unresolvable reasons. `:not_in_bundle` covers both the "RubyGems doesn't know this gem" case and the
|
|
@@ -43,6 +50,20 @@ module Rigor
|
|
|
43
50
|
)
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
# Issue #1814 — the {Resolved#dependency_entry} of each non-disabled `dependencies.source_inference` entry that
|
|
54
|
+
# resolves, in configuration order, without walking any gem. The run-result cache key and the incremental
|
|
55
|
+
# snapshot's fingerprint are built before analysis from inputs that are cheap to read, and a gem's walked
|
|
56
|
+
# source (its method catalogue, its refinement table, the constants it binds) is an input of the run that no
|
|
57
|
+
# analysed file records; a `Gemfile.lock` digest covers a `bundle update` and nothing else, so a project with no
|
|
58
|
+
# lockfile, or a gem installed beside the bundle, would replay the diagnostics of the gem's previous version.
|
|
59
|
+
# Unresolvable entries have no version to key on and add nothing: the gem appearing later adds its row.
|
|
60
|
+
def dependency_entries(dependencies)
|
|
61
|
+
dependencies.source_inference.reject(&:disabled?).filter_map do |entry|
|
|
62
|
+
resolved = resolve(entry)
|
|
63
|
+
resolved.dependency_entry if resolved.is_a?(Resolved)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
46
67
|
# Locator. `Gem.loaded_specs` reflects the bundle (cheap lookup, no filesystem walk); `find_by_name` is
|
|
47
68
|
# the broader fallback for gems present on the gem path but not yet `require`'d. `Gem::MissingSpecError`
|
|
48
69
|
# is a `LoadError` subclass, so the rescue covers both missing-spec and load-error signals.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../cache/descriptor"
|
|
4
|
+
require_relative "../../inference/scope_indexer"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Analysis
|
|
@@ -11,7 +12,8 @@ module Rigor
|
|
|
11
12
|
# entries.
|
|
12
13
|
class Index
|
|
13
14
|
attr_reader :resolved_gems, :unresolvable, :method_catalog, :budget_exceeded,
|
|
14
|
-
:class_to_gem, :budget_overrun_strategy, :gem_modes
|
|
15
|
+
:class_to_gem, :budget_overrun_strategy, :gem_modes, :refinements, :bound_constants,
|
|
16
|
+
:bound_segments
|
|
15
17
|
|
|
16
18
|
# @param method_catalog — => Symbol}] the flat
|
|
17
19
|
# `(class_name, method_name) → :instance | :singleton` table produced by {Walker.walk}, aggregated
|
|
@@ -30,10 +32,21 @@ module Rigor
|
|
|
30
32
|
# gem-source and RBS both contribute under `mode: :full`. The map is keyed on `gem_name` (not
|
|
31
33
|
# class) because re-opened classes belong to the first gem they appeared in per `class_to_gem`;
|
|
32
34
|
# `mode_for(class_name)` chains the two lookups.
|
|
33
|
-
|
|
35
|
+
# @param refinements — issue #1672: the opt-in gems' `refine X do … end` bodies, `{refined class =>
|
|
36
|
+
# {method => [refining modules]}}` ({Walker::Outcome#refinements}). The runner merges it into the
|
|
37
|
+
# project's `discovered_refinements` seed, so `using M` for a gem's `M` reaches the check rules.
|
|
38
|
+
# @param bound_constants — issue #1814: every name the opt-in gems' files bind by a constant write without
|
|
39
|
+
# declaring a class or module by it ({Walker::Outcome#bound}: `Shared::Target` for `module Shared; Target =
|
|
40
|
+
# String; end`), unioned across the walked gems. A refine key one of them binds may name any value
|
|
41
|
+
# (`refine Shared::Target` in the project or in the next gem), which the reader decides at read time
|
|
42
|
+
# ({Inference::InEffectRefinements.class_wildcard_key?}) through {#bound_constant?}.
|
|
43
|
+
# @param bound_segments — the last segment of each such write whose owner the walk cannot spell
|
|
44
|
+
# ({Walker::Outcome#bound_segments}: a `const_set`), which matches that segment of any key.
|
|
45
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
34
46
|
resolved_gems: [], unresolvable: [], method_catalog: {},
|
|
35
47
|
budget_exceeded: [], class_to_gem: {},
|
|
36
|
-
budget_overrun_strategy: :walker_cap, gem_modes: {}
|
|
48
|
+
budget_overrun_strategy: :walker_cap, gem_modes: {}, refinements: {}, bound_constants: Set.new,
|
|
49
|
+
bound_segments: Set.new
|
|
37
50
|
)
|
|
38
51
|
@resolved_gems = resolved_gems.freeze
|
|
39
52
|
@unresolvable = unresolvable.freeze
|
|
@@ -42,6 +55,9 @@ module Rigor
|
|
|
42
55
|
@class_to_gem = class_to_gem.freeze
|
|
43
56
|
@budget_overrun_strategy = budget_overrun_strategy
|
|
44
57
|
@gem_modes = gem_modes.freeze
|
|
58
|
+
@refinements = Inference::ScopeIndexer.freeze_refinements(refinements)
|
|
59
|
+
@bound_constants = bound_constants.to_set.freeze
|
|
60
|
+
@bound_segments = bound_segments.to_set.freeze
|
|
45
61
|
freeze
|
|
46
62
|
end
|
|
47
63
|
|
|
@@ -62,6 +78,22 @@ module Rigor
|
|
|
62
78
|
@class_to_gem[class_name]
|
|
63
79
|
end
|
|
64
80
|
|
|
81
|
+
# Issue #1814 — does a walked gem bind `name` (a refine key, or a prefix of one: `Shared::Target`, `Pag::Enc`)
|
|
82
|
+
# by a constant write that declares no class or module, or bind a constant of `name`'s last segment under an
|
|
83
|
+
# owner the walk could not spell? Such a key may name any value (`module Shared; Target = String; end` then
|
|
84
|
+
# `refine(Shared::Target)`), which the walker cannot resolve and the reader decides
|
|
85
|
+
# ({Inference::InEffectRefinements.class_wildcard_key?}). A gem constant that merely shares a core class's name
|
|
86
|
+
# (`Names::String`) binds nothing a sibling spelling of `refine String` (`Ext::String`) could denote.
|
|
87
|
+
def bound_constant?(name)
|
|
88
|
+
@bound_constants.include?(name) || @bound_segments.include?(name.split("::").last)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Does any walked gem bind a constant at all? False for a project that opts in no gem, so a reader asks nothing
|
|
92
|
+
# per key.
|
|
93
|
+
def binds_constants?
|
|
94
|
+
!(@bound_constants.empty? && @bound_segments.empty?)
|
|
95
|
+
end
|
|
96
|
+
|
|
65
97
|
# ADR-10 slice 5c — per-class mode lookup. Chains `class_to_gem` + `gem_modes`; returns `nil` when
|
|
66
98
|
# the class isn't owned by any opt-in gem in this run.
|
|
67
99
|
def mode_for(class_name)
|
|
@@ -99,14 +131,7 @@ module Rigor
|
|
|
99
131
|
# surfaces them as `dynamic.dependency-source.gem-not-found` diagnostics. Resolved-but-disabled
|
|
100
132
|
# entries are also absent: the {Builder} skips them before resolution, so they never reach the index.
|
|
101
133
|
def cache_descriptor
|
|
102
|
-
dependencies
|
|
103
|
-
Cache::Descriptor::DependencyEntry.new(
|
|
104
|
-
gem_name: resolved.gem_name,
|
|
105
|
-
gem_version: resolved.version,
|
|
106
|
-
mode: resolved.mode
|
|
107
|
-
)
|
|
108
|
-
end
|
|
109
|
-
Cache::Descriptor.new(dependencies: dependencies)
|
|
134
|
+
Cache::Descriptor.new(dependencies: @resolved_gems.map(&:dependency_entry))
|
|
110
135
|
end
|
|
111
136
|
end
|
|
112
137
|
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "return_type_heuristic"
|
|
6
6
|
require_relative "../../source/constant_path"
|
|
7
7
|
require_relative "../../source/node_children"
|
|
8
|
+
require_relative "../../inference/scope_indexer"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Analysis
|
|
@@ -35,10 +36,47 @@ module Rigor
|
|
|
35
36
|
# `budget`, and `truncated?` reports whether the cap was reached. The Index records this per-gem so
|
|
36
37
|
# the Runner can surface a single `dynamic.dependency-source.budget-exceeded` warning naming the
|
|
37
38
|
# affected gem(s).
|
|
38
|
-
|
|
39
|
+
#
|
|
40
|
+
# Issue #1672 — `refinements` is the gem's `refine X do … end` table, `{refined class => {method =>
|
|
41
|
+
# [refining modules]}}`, the shape `Inference::ScopeIndexer` builds for project files. A refine-body `def`
|
|
42
|
+
# goes there and never into `catalog`: it is not a method of the refining module, and of the refined class
|
|
43
|
+
# only after `using`. ADR-121 WD7 (issue #1814) — the table is the one the project walk's census records for
|
|
44
|
+
# the same source ({Inference::ScopeIndexer.refinement_table}), file by file, so it carries the wildcard rows
|
|
45
|
+
# a project module's would.
|
|
46
|
+
#
|
|
47
|
+
# Issue #1814 — `bound` is every name the gem's files bind by a constant write (an assignment, an operator
|
|
48
|
+
# write, a multiple assignment, a rooted path write) without declaring a class or module by it, spelled under
|
|
49
|
+
# the lexical nesting the write sits in (`Shared::Target` for `module Shared; Target = String; end`);
|
|
50
|
+
# `bound_segments` is the last segment of each write whose owner the walk cannot spell (a path write that is
|
|
51
|
+
# not rooted, a write under `class << …`, a `const_set` with a literal name) ({.collect_bound_constants}). A
|
|
52
|
+
# refine key one of these binds (`refine(Shared::Target)` anywhere) may name any value, which the walker cannot
|
|
53
|
+
# resolve: the rows stay as the census records them, {Index#bound_constant?} unions the names across the walked
|
|
54
|
+
# gems, and the reader decides the key at read time, by each prefix of it
|
|
55
|
+
# ({Inference::InEffectRefinements.class_wildcard_key?}).
|
|
56
|
+
class Outcome < Data.define(:catalog, :truncated, :refinements, :bound, :bound_segments)
|
|
57
|
+
def initialize(catalog:, truncated:, refinements: {}.freeze, bound: Set.new.freeze,
|
|
58
|
+
bound_segments: Set.new.freeze)
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
39
62
|
def truncated? = truncated
|
|
40
63
|
end
|
|
41
64
|
|
|
65
|
+
# The walk's accumulators. The budget counts `catalog` only, but stops the whole walk: a file reached after it
|
|
66
|
+
# trips is not read. `root` is the file being walked and `in_effect` its {Inference::InEffectRefinements}, built
|
|
67
|
+
# the first time a `refine`-shaped call asks (issue #1689). Issue #1814 — `bound` and `bound_segments` are
|
|
68
|
+
# {Outcome}'s.
|
|
69
|
+
Harvest = Struct.new(:catalog, :refinements, :root, :in_effect, :bound, :bound_segments) do
|
|
70
|
+
# {Inference::ScopeIndexer.module_refine_target} against this file's in-effect refinements.
|
|
71
|
+
def refine_target_of(node)
|
|
72
|
+
return nil if Inference::ScopeIndexer.refine_target(node).nil?
|
|
73
|
+
|
|
74
|
+
self.in_effect ||= Inference::InEffectRefinements.new(root)
|
|
75
|
+
Inference::ScopeIndexer.module_refine_target(node, in_effect)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
private_constant :Harvest
|
|
79
|
+
|
|
42
80
|
# Per-method catalog entry. `kind` is `:instance` or `:singleton`; `return_type` is the
|
|
43
81
|
# {ReturnTypeHeuristic}-extracted static facet (a `Rigor::Type::*`) or `nil` when the heuristic
|
|
44
82
|
# declined. The dispatcher wraps a non-nil `return_type` in `Dynamic[T]`; a `nil` `return_type` falls
|
|
@@ -65,14 +103,16 @@ module Rigor
|
|
|
65
103
|
# the budget was reached. Methods of identical name on the same class with different kinds (rare;
|
|
66
104
|
# private API mostly) carry the kind that wins the per-class first walk.
|
|
67
105
|
def walk(gem_dir:, roots:, budget: UNBOUNDED)
|
|
68
|
-
|
|
106
|
+
harvest = Harvest.new({}, {}, nil, nil, Set.new, Set.new)
|
|
69
107
|
truncated = false
|
|
70
108
|
accepted_roots(roots).each do |root|
|
|
71
109
|
break if truncated
|
|
72
110
|
|
|
73
|
-
truncated = walk_root(File.join(gem_dir.to_s, root),
|
|
111
|
+
truncated = walk_root(File.join(gem_dir.to_s, root), harvest, budget)
|
|
74
112
|
end
|
|
75
|
-
Outcome.new(catalog:
|
|
113
|
+
Outcome.new(catalog: harvest.catalog.freeze, truncated: truncated,
|
|
114
|
+
refinements: Inference::ScopeIndexer.freeze_refinements(harvest.refinements),
|
|
115
|
+
bound: harvest.bound.freeze, bound_segments: harvest.bound_segments.freeze)
|
|
76
116
|
end
|
|
77
117
|
|
|
78
118
|
# Drops hard-excluded entries before any filesystem walk happens. Reasoning: we never want a gem's
|
|
@@ -84,21 +124,25 @@ module Rigor
|
|
|
84
124
|
|
|
85
125
|
# Returns true when the budget tripped during this root's walk so the caller can stop iterating
|
|
86
126
|
# subsequent roots.
|
|
87
|
-
def walk_root(root_dir,
|
|
127
|
+
def walk_root(root_dir, harvest, budget) # rubocop:disable Naming/PredicateMethod
|
|
88
128
|
return false unless File.directory?(root_dir)
|
|
89
129
|
|
|
90
130
|
Dir.glob(File.join(root_dir, "**", "*.rb")).each do |path|
|
|
91
|
-
harvest_file(path,
|
|
92
|
-
return true if
|
|
131
|
+
harvest_file(path, harvest, budget)
|
|
132
|
+
return true if harvest.catalog.size >= budget
|
|
93
133
|
end
|
|
94
134
|
false
|
|
95
135
|
end
|
|
96
136
|
|
|
97
|
-
def harvest_file(path,
|
|
137
|
+
def harvest_file(path, harvest, budget)
|
|
98
138
|
parse_result = Prism.parse_file(path)
|
|
99
139
|
return unless parse_result.errors.empty?
|
|
100
140
|
|
|
101
|
-
|
|
141
|
+
harvest.root = parse_result.value
|
|
142
|
+
harvest.in_effect = nil
|
|
143
|
+
record_refinements(parse_result.value, path, harvest)
|
|
144
|
+
collect_bound_constants(parse_result.value, [[]], harvest)
|
|
145
|
+
walk_node(parse_result.value, [], false, harvest, budget)
|
|
102
146
|
rescue StandardError
|
|
103
147
|
# Gem source we can't parse / read silently degrades to "no contribution from this file". The
|
|
104
148
|
# user-facing diagnostic stream is reserved for the project source; opt-in gem source MUST NOT
|
|
@@ -106,66 +150,191 @@ module Rigor
|
|
|
106
150
|
nil
|
|
107
151
|
end
|
|
108
152
|
|
|
153
|
+
# ADR-121 WD7 (issue #1814) — the refinement table the file contributes, read by the census the project walk
|
|
154
|
+
# runs ({Inference::ScopeIndexer.refinement_table}): the `refine` bodies, the computed targets, the `refine`
|
|
155
|
+
# inside a method, the `:refine` literals, and the wildcard rows each leaves, folded as the project folds its
|
|
156
|
+
# files' tables (`merge_refinement_tables`). The walker's own descent below records no row.
|
|
157
|
+
def record_refinements(root, path, harvest)
|
|
158
|
+
harvest.refinements = Inference::ScopeIndexer.merge_refinement_tables(
|
|
159
|
+
harvest.refinements, Inference::ScopeIndexer.refinement_table(root, path)
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Issue #1814 — the names the file binds by a constant write without declaring a class or module by them, or
|
|
164
|
+
# the last segment alone where the walk cannot spell the owner Ruby binds. A bare write (`K = …`, `K ||= …`,
|
|
165
|
+
# `K += …`, a multiple-assignment target) binds the name under the lexical nesting of the `class` / `module`
|
|
166
|
+
# headers around it, which a block does not change (a `Module.new do … end` or `module_eval { … }` body assigns
|
|
167
|
+
# a constant in the lexical scope around it); `prefixes` holds every nesting the headers can denote, since a
|
|
168
|
+
# nested compact header opens whichever module Ruby finds (`module W; module Shared::Inner` is
|
|
169
|
+
# `W::Shared::Inner` or `Shared::Inner`), as the census spells a refine target. A write whose value is one of
|
|
170
|
+
# the class-creating shapes the project walk registers as a declaration (`Class.new`, `Module.new`,
|
|
171
|
+
# `Struct.new`, `Data.define`, a `.freeze` tail or a `K = K || …` guard stripped:
|
|
172
|
+
# {Inference::ScopeIndexer.meta_new_rvalue}) declares a class, so `Point = Struct.new(:x); refine(Point)` keeps
|
|
173
|
+
# a normal row. Where the owner cannot be spelled the segment alone is recorded, which matches that segment of
|
|
174
|
+
# any key: a write under `class << …` (the singleton's), a path write that is not rooted (`Wrap::K = …` may
|
|
175
|
+
# bind under an included module's `Wrap`; `self::K = …` in a block binds under the block's `self`), and a
|
|
176
|
+
# `const_set` with a literal name (`Object.const_set(:K, …)` binds `::K`). A rooted path (`::Shared::K = …`)
|
|
177
|
+
# binds that name. One pass over the tree; the rows are not touched.
|
|
178
|
+
def collect_bound_constants(node, prefixes, harvest)
|
|
179
|
+
return unless node.is_a?(Prism::Node)
|
|
180
|
+
|
|
181
|
+
case node
|
|
182
|
+
when Prism::ClassNode, Prism::ModuleNode then prefixes = header_prefixes(node.constant_path, prefixes)
|
|
183
|
+
when Prism::SingletonClassNode then prefixes = nil
|
|
184
|
+
when Prism::ConstantWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
|
|
185
|
+
Prism::ConstantOperatorWriteNode
|
|
186
|
+
note_bare_write(node.name.to_s, prefixes, harvest) unless declares_class?(node)
|
|
187
|
+
when Prism::ConstantTargetNode then note_bare_write(node.name.to_s, prefixes, harvest)
|
|
188
|
+
when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
|
|
189
|
+
Prism::ConstantPathOperatorWriteNode
|
|
190
|
+
note_path_write(node.target, harvest) unless declares_class?(node)
|
|
191
|
+
when Prism::ConstantPathTargetNode then note_path_write(node, harvest)
|
|
192
|
+
when Prism::CallNode then note_const_set(node, harvest)
|
|
193
|
+
end
|
|
194
|
+
node.rigor_each_child { |child| collect_bound_constants(child, prefixes, harvest) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def declares_class?(write)
|
|
198
|
+
rvalue = Inference::ScopeIndexer.meta_new_rvalue(write)
|
|
199
|
+
!rvalue.nil? && Inference::ScopeIndexer.meta_new_constant_rvalue?(rvalue)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# The nestings a `class` / `module` body may sit in, given the ones its header is written in: a bare header
|
|
203
|
+
# opens a module under each; a compact header (`module Shared::Inner`) opens `Inner` under every name its base
|
|
204
|
+
# can denote there ({Inference::ScopeIndexer.constant_receiver_candidates}); a rooted header resets them. nil,
|
|
205
|
+
# the unspellable owner, for a header no constant spells or one under `class << …`.
|
|
206
|
+
def header_prefixes(constant_path, prefixes)
|
|
207
|
+
return nil if prefixes.nil? || Source::ConstantPath.qualified_name_or_nil(constant_path).nil?
|
|
208
|
+
return [Source::ConstantPath.qualified_name_or_nil(constant_path).split("::")] if rooted_path?(constant_path)
|
|
209
|
+
|
|
210
|
+
name = constant_path.name.to_s
|
|
211
|
+
return prefixes.map { |prefix| prefix + [name] } if constant_path.is_a?(Prism::ConstantReadNode)
|
|
212
|
+
|
|
213
|
+
prefixes.flat_map do |prefix|
|
|
214
|
+
Inference::ScopeIndexer.constant_receiver_candidates(constant_path.parent, prefix)
|
|
215
|
+
.map { |owner| owner.split("::") + [name] }
|
|
216
|
+
end.uniq
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# `::K`, `::A::K`, as a path or a multiple-assignment target: the leftmost segment has no parent.
|
|
220
|
+
def rooted_path?(path)
|
|
221
|
+
path.respond_to?(:parent) && (path.parent.nil? || Source::ConstantPath.rooted?(path.parent))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def note_bare_write(name, prefixes, harvest)
|
|
225
|
+
return harvest.bound_segments << name if prefixes.nil?
|
|
226
|
+
|
|
227
|
+
prefixes.each { |prefix| harvest.bound << (prefix + [name]).join("::") }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# A path write binds its name where its base resolves, which the walk can spell only for a rooted base.
|
|
231
|
+
def note_path_write(path, harvest)
|
|
232
|
+
name = path.name.to_s
|
|
233
|
+
return harvest.bound_segments << name unless rooted_path?(path)
|
|
234
|
+
|
|
235
|
+
owner = path.parent && Source::ConstantPath.qualified_name_or_nil(path.parent)
|
|
236
|
+
harvest.bound << (owner ? "#{owner}::#{name}" : name)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# `const_set(:K, value)` / `const_set("K", value)` with a literal name, on any receiver: the owner is a value.
|
|
240
|
+
# A class-creating value declares a class, as a write of one does.
|
|
241
|
+
def note_const_set(node, harvest)
|
|
242
|
+
return unless node.name == :const_set
|
|
243
|
+
|
|
244
|
+
name, value = node.arguments&.arguments
|
|
245
|
+
return unless name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
|
|
246
|
+
return if value && Inference::ScopeIndexer.meta_new_constant_rvalue?(value)
|
|
247
|
+
|
|
248
|
+
harvest.bound_segments << name.unescaped
|
|
249
|
+
end
|
|
250
|
+
|
|
109
251
|
# Walks a Prism subtree, accumulating method definitions under their qualified class name. Mirrors
|
|
110
252
|
# the shape of `Inference::ScopeIndexer#walk_methods` but stays decoupled from `Scope` because
|
|
111
253
|
# gem-source inference runs without a scope context.
|
|
112
|
-
def walk_node(node, qualified_prefix, in_singleton_class,
|
|
254
|
+
def walk_node(node, qualified_prefix, in_singleton_class, harvest, budget)
|
|
113
255
|
return unless node.is_a?(Prism::Node)
|
|
114
|
-
return if
|
|
256
|
+
return if harvest.catalog.size >= budget
|
|
115
257
|
|
|
116
258
|
case node
|
|
117
259
|
when Prism::ClassNode, Prism::ModuleNode
|
|
118
|
-
descend_class_or_module(node, qualified_prefix, in_singleton_class,
|
|
260
|
+
descend_class_or_module(node, qualified_prefix, in_singleton_class, harvest, budget)
|
|
119
261
|
when Prism::SingletonClassNode
|
|
120
|
-
descend_singleton_class(node, qualified_prefix,
|
|
262
|
+
descend_singleton_class(node, qualified_prefix, harvest, budget)
|
|
121
263
|
when Prism::DefNode
|
|
122
|
-
record_def_node(node, qualified_prefix, in_singleton_class,
|
|
264
|
+
record_def_node(node, qualified_prefix, in_singleton_class, harvest, budget)
|
|
265
|
+
when Prism::CallNode
|
|
266
|
+
return walk_refine_body(node, qualified_prefix, harvest, budget) if harvest.refine_target_of(node)
|
|
267
|
+
|
|
268
|
+
walk_children(node, qualified_prefix, in_singleton_class, harvest, budget)
|
|
123
269
|
else
|
|
124
|
-
walk_children(node, qualified_prefix, in_singleton_class,
|
|
270
|
+
walk_children(node, qualified_prefix, in_singleton_class, harvest, budget)
|
|
125
271
|
end
|
|
126
272
|
end
|
|
127
273
|
|
|
128
|
-
def walk_children(node, qualified_prefix, in_singleton_class,
|
|
274
|
+
def walk_children(node, qualified_prefix, in_singleton_class, harvest, budget)
|
|
129
275
|
node.rigor_each_child do |child|
|
|
130
|
-
break if
|
|
276
|
+
break if harvest.catalog.size >= budget
|
|
131
277
|
|
|
132
|
-
walk_node(child, qualified_prefix, in_singleton_class,
|
|
278
|
+
walk_node(child, qualified_prefix, in_singleton_class, harvest, budget)
|
|
133
279
|
end
|
|
134
280
|
end
|
|
135
281
|
|
|
136
282
|
# `class Foo` / `module Bar`. The dynamic-prefix shape (`module ::Foo`-rooted variants whose left
|
|
137
283
|
# side is a runtime expression) is treated as opaque — we walk the children under the same prefix
|
|
138
284
|
# so any inner class definitions are still recorded under their own name.
|
|
139
|
-
def descend_class_or_module(node, qualified_prefix, in_singleton_class,
|
|
285
|
+
def descend_class_or_module(node, qualified_prefix, in_singleton_class, harvest, budget)
|
|
140
286
|
name = Source::ConstantPath.qualified_name_or_nil(node.constant_path)
|
|
141
287
|
if name && node.body
|
|
142
288
|
child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
143
|
-
walk_node(node.body, child_prefix, in_singleton_class,
|
|
289
|
+
walk_node(node.body, child_prefix, in_singleton_class, harvest, budget)
|
|
144
290
|
else
|
|
145
|
-
walk_children(node, qualified_prefix, in_singleton_class,
|
|
291
|
+
walk_children(node, qualified_prefix, in_singleton_class, harvest, budget)
|
|
146
292
|
end
|
|
147
293
|
end
|
|
148
294
|
|
|
149
295
|
# `class << self` only — `class << expr` for any other `expr` is treated as opaque so we don't
|
|
150
296
|
# accidentally record per-instance singleton methods under the surrounding class.
|
|
151
|
-
def descend_singleton_class(node, qualified_prefix,
|
|
297
|
+
def descend_singleton_class(node, qualified_prefix, harvest, budget)
|
|
152
298
|
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
153
|
-
walk_node(node.body, qualified_prefix, true,
|
|
299
|
+
walk_node(node.body, qualified_prefix, true, harvest, budget)
|
|
154
300
|
else
|
|
155
|
-
walk_children(node, qualified_prefix, false,
|
|
301
|
+
walk_children(node, qualified_prefix, false, harvest, budget)
|
|
156
302
|
end
|
|
157
303
|
end
|
|
158
304
|
|
|
159
|
-
def record_def_node(node, qualified_prefix, in_singleton_class,
|
|
305
|
+
def record_def_node(node, qualified_prefix, in_singleton_class, harvest, _budget)
|
|
160
306
|
return if qualified_prefix.empty?
|
|
161
307
|
|
|
162
308
|
class_name = qualified_prefix.join("::")
|
|
163
309
|
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
164
310
|
key = [class_name, node.name]
|
|
165
|
-
return if
|
|
311
|
+
return if harvest.catalog.key?(key) # first walk wins
|
|
166
312
|
|
|
167
313
|
return_type = ReturnTypeHeuristic.extract(node)
|
|
168
|
-
|
|
314
|
+
harvest.catalog[key] = CatalogEntry.new(kind: kind, return_type: return_type)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Issue #1672 — `refine X do … end`, the shape {Inference::ScopeIndexer.module_refine_target} accepts (not one
|
|
318
|
+
# whose `self` is a class: that is the class's own method and walks as any other call, issue #1689). The body's
|
|
319
|
+
# instance `def`s are refinements of X by the enclosing module, and none reaches the catalogue. The refinement
|
|
320
|
+
# table is not built here: {.record_refinements} runs the project walk's census over the whole file, which reads
|
|
321
|
+
# this body and every other `refine`-shaped node (ADR-121 WD7, issue #1814). Declarations nested in the body
|
|
322
|
+
# still walk under the lexical prefix, as they did before.
|
|
323
|
+
def walk_refine_body(node, qualified_prefix, harvest, budget)
|
|
324
|
+
body = node.block.body
|
|
325
|
+
walk_refine_declarations(body, qualified_prefix, harvest, budget) unless body.nil?
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# The class / module declarations inside a refine body, walked as before; nothing else in it is.
|
|
329
|
+
def walk_refine_declarations(node, qualified_prefix, harvest, budget)
|
|
330
|
+
case node
|
|
331
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
332
|
+
walk_node(node, qualified_prefix, false, harvest, budget)
|
|
333
|
+
when Prism::DefNode, Prism::SingletonClassNode
|
|
334
|
+
nil
|
|
335
|
+
else
|
|
336
|
+
node.rigor_each_child { |child| walk_refine_declarations(child, qualified_prefix, harvest, budget) }
|
|
337
|
+
end
|
|
169
338
|
end
|
|
170
339
|
end
|
|
171
340
|
end
|
|
@@ -25,7 +25,8 @@ module Rigor
|
|
|
25
25
|
source = parse_result.source.source
|
|
26
26
|
return false unless source.is_a?(String) && !source.empty?
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# The marker is ASCII; match bytes so an invalid byte elsewhere in the file cannot raise.
|
|
29
|
+
ERB_CLOSING_MARKER.match?(source.b)
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
32
|
end
|
|
@@ -120,18 +120,22 @@ module Rigor
|
|
|
120
120
|
appeared.freeze
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
# Issue #1120 — the method names whose refinements moved in this edit
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
123
|
+
# Issue #1120 — the method names whose refinements moved in this edit. A consumer whose answer consulted the
|
|
124
|
+
# refinement table recorded a `refinement:<method>` edge whichever way it answered, because the answer is a
|
|
125
|
+
# function of the whole project's refinements of that name. `paths` MUST include removed files, whose whole
|
|
126
|
+
# before-table vanished. `before` / `after` map a path to that file's `{refined class => {method =>
|
|
127
|
+
# [modules]}}` table (nil when it refines nothing).
|
|
128
|
+
#
|
|
129
|
+
# Issue #1664 — every name a changed file refines, before or after the edit, not only the entries that
|
|
130
|
+
# appeared or vanished: the typed refined arm answers with a refine body's inferred return, which an edit
|
|
131
|
+
# inside the body changes while the table stays the same. A changed file's refinements are therefore all
|
|
132
|
+
# re-checked, which over-reports an edit elsewhere in the file and costs only the consumers of its names.
|
|
129
133
|
def changed_refinement_names(paths, before, after)
|
|
130
134
|
moved = Set.new
|
|
131
135
|
paths.each do |path|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
(refinement_entries(before[path]) | refinement_entries(after[path])).each do |(_refined, method_name, _)|
|
|
137
|
+
moved << method_name
|
|
138
|
+
end
|
|
135
139
|
end
|
|
136
140
|
moved.freeze
|
|
137
141
|
end
|
|
@@ -146,6 +150,22 @@ module Rigor
|
|
|
146
150
|
entries
|
|
147
151
|
end
|
|
148
152
|
|
|
153
|
+
# Issue #1820 — adds to `per_file` (`{path => Set}`) a frozen `[class, superclass as written]` pair in each file
|
|
154
|
+
# that declares a class with a superclass, so {changed_class_declarations} reports a subclass edge appearing or
|
|
155
|
+
# vanishing beside the class names.
|
|
156
|
+
def add_superclass_edges(per_file, class_sources, superclasses)
|
|
157
|
+
return per_file if superclasses.nil? || superclasses.empty?
|
|
158
|
+
|
|
159
|
+
superclasses.each do |child, parent|
|
|
160
|
+
edge = [child, parent.nil? ? "*" : parent.to_s].freeze
|
|
161
|
+
(class_sources[child] || EMPTY_FILES).each { |file| per_file[file] << edge }
|
|
162
|
+
end
|
|
163
|
+
per_file
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
EMPTY_FILES = [].freeze
|
|
167
|
+
private_constant :EMPTY_FILES
|
|
168
|
+
|
|
149
169
|
# Issue #639 — the SYMMETRIC reading of {appeared_classes}: the names whose declaration in a file
|
|
150
170
|
# appeared OR VANISHED. Appearance alone answered the miss side, which was the only side recording a
|
|
151
171
|
# `class:` edge; a bare reference that RESOLVED now records one too, and what invalidates it is the
|