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
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "rule_ids"
|
|
6
|
+
require_relative "../diagnostic"
|
|
7
|
+
require_relative "../../inference/version_guard"
|
|
8
|
+
require_relative "../../source/constant_path"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Analysis
|
|
12
|
+
module CheckRules
|
|
13
|
+
# Issue #1692 (the ADR-47 WD5 amendment) — `call.deprecated-ruby2-keywords`: a call to a method of the
|
|
14
|
+
# `ruby2_keywords` family, which Ruby 4.1 deprecates (Feature #22205). `Module#ruby2_keywords`, top-level
|
|
15
|
+
# `ruby2_keywords` and `Proc#ruby2_keywords` are removed in Ruby 4.4, `Hash.ruby2_keywords_hash?` and
|
|
16
|
+
# `Hash.ruby2_keywords_hash` in Ruby 4.5.
|
|
17
|
+
#
|
|
18
|
+
# The rule runs only when `.rigor.yml` states the runtime: `target_ruby` set explicitly to 4.1 or later
|
|
19
|
+
# ({Configuration#stated_runtime_ruby}). On Ruby 4.0 the same calls are correct and silent, so the default
|
|
20
|
+
# `target_ruby` fires nothing. The file walk is paid only by a project that states such a target.
|
|
21
|
+
#
|
|
22
|
+
# A call counts only where it can reach the core method and nothing else:
|
|
23
|
+
#
|
|
24
|
+
# - `ruby2_keywords` with no receiver (or `self`), in a class or module body, a `class << self` body or a
|
|
25
|
+
# singleton method, where `self` is a Module. At the top level outside any block, it is the `main`
|
|
26
|
+
# method (a top-level `def` body included). Inside a top-level block, inside any `instance_eval` /
|
|
27
|
+
# `instance_exec` block, and in an instance method of a class or module, it is not judged.
|
|
28
|
+
# - `ruby2_keywords` on a receiver typed `Proc`.
|
|
29
|
+
# - `ruby2_keywords_hash?` / `ruby2_keywords_hash` on `Hash` or a class RBS knows as its subclass, or with
|
|
30
|
+
# no receiver where `self` is one.
|
|
31
|
+
# - The same names through `send` / `__send__` with a literal Symbol, which also reach the private
|
|
32
|
+
# `Module#ruby2_keywords` on an explicit Module receiver.
|
|
33
|
+
#
|
|
34
|
+
# A project or `pre_eval:` file that defines a method by the same name anywhere silences that name, since
|
|
35
|
+
# the call may then reach the project's method. A call in an arm of a version guard that cannot run on the
|
|
36
|
+
# stated Ruby, or whose guard cannot be decided, is not reported, nor are the statements after a guard
|
|
37
|
+
# that jumps away (`return if RUBY_VERSION < "2.7"`).
|
|
38
|
+
module RubyDeprecations
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# The first Ruby that deprecates the family.
|
|
42
|
+
DEPRECATED_SINCE = "4.1"
|
|
43
|
+
|
|
44
|
+
MODULE_FORM = ["Module#ruby2_keywords", "4.4"].freeze
|
|
45
|
+
TOPLEVEL_FORM = ["top-level ruby2_keywords", "4.4"].freeze
|
|
46
|
+
PROC_FORM = ["Proc#ruby2_keywords", "4.4"].freeze
|
|
47
|
+
HASH_FORMS = {
|
|
48
|
+
ruby2_keywords_hash?: ["Hash.ruby2_keywords_hash?", "4.5"].freeze,
|
|
49
|
+
ruby2_keywords_hash: ["Hash.ruby2_keywords_hash", "4.5"].freeze
|
|
50
|
+
}.freeze
|
|
51
|
+
NAMES = (%i[ruby2_keywords] + HASH_FORMS.keys).freeze
|
|
52
|
+
SEND_NAMES = %i[send __send__].freeze
|
|
53
|
+
INSTANCE_EVAL_NAMES = %i[instance_eval instance_exec].freeze
|
|
54
|
+
private_constant :MODULE_FORM, :TOPLEVEL_FORM, :PROC_FORM, :HASH_FORMS, :NAMES, :SEND_NAMES,
|
|
55
|
+
:INSTANCE_EVAL_NAMES
|
|
56
|
+
|
|
57
|
+
# The walk's lexical context: whether a block hides `self`, and whether a version guard makes the node
|
|
58
|
+
# unreportable.
|
|
59
|
+
Context = Data.define(:in_block, :in_instance_eval, :guarded)
|
|
60
|
+
ROOT_CONTEXT = Context.new(in_block: false, in_instance_eval: false, guarded: false)
|
|
61
|
+
private_constant :Context, :ROOT_CONTEXT
|
|
62
|
+
|
|
63
|
+
# @param stated_ruby — {Configuration#stated_runtime_ruby}, or nil
|
|
64
|
+
# @return whether the rule runs at all for this target
|
|
65
|
+
def active?(stated_ruby)
|
|
66
|
+
return false if stated_ruby.nil? || !::Gem::Version.correct?(stated_ruby)
|
|
67
|
+
|
|
68
|
+
::Gem::Version.new(stated_ruby) >= ::Gem::Version.new(DEPRECATED_SINCE)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def diagnostics(path, root, scope_index, stated_ruby)
|
|
72
|
+
return [] unless active?(stated_ruby)
|
|
73
|
+
|
|
74
|
+
guard_ruby = guard_version(stated_ruby)
|
|
75
|
+
results = []
|
|
76
|
+
shadowed = {}
|
|
77
|
+
walk(root, ROOT_CONTEXT, guard_ruby) do |call, context|
|
|
78
|
+
form = deprecated_form(call, context, scope_index, shadowed)
|
|
79
|
+
results << build(path, call, form, stated_ruby) if form
|
|
80
|
+
end
|
|
81
|
+
results
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# `RUBY_VERSION` reads `"4.1.0"` on Ruby 4.1, so a stated `"4.1"` is padded before a guard compares it
|
|
85
|
+
# as a String.
|
|
86
|
+
def guard_version(stated_ruby)
|
|
87
|
+
segments = stated_ruby.split(".")
|
|
88
|
+
(segments + (["0"] * (3 - segments.size))).join(".")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def walk(node, context, guard_ruby, &)
|
|
92
|
+
# Nothing under a guarded node is reported, so the walk does not descend into it.
|
|
93
|
+
return if !node.is_a?(Prism::Node) || context.guarded
|
|
94
|
+
|
|
95
|
+
yield node, context if node.is_a?(Prism::CallNode) && candidate?(node)
|
|
96
|
+
|
|
97
|
+
jumped = false
|
|
98
|
+
node.rigor_each_child do |child|
|
|
99
|
+
child_context = child_context(node, child, context, guard_ruby)
|
|
100
|
+
child_context = child_context.with(guarded: true) if jumped
|
|
101
|
+
walk(child, child_context, guard_ruby, &)
|
|
102
|
+
jumped ||= node.is_a?(Prism::StatementsNode) && jumping_version_guard?(child)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def candidate?(call)
|
|
107
|
+
return true if NAMES.include?(call.name)
|
|
108
|
+
|
|
109
|
+
SEND_NAMES.include?(call.name) && NAMES.include?(send_target(call))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The Symbol a `send` names as its first argument, or nil.
|
|
113
|
+
def send_target(call)
|
|
114
|
+
first = call.arguments&.arguments&.first
|
|
115
|
+
first.is_a?(Prism::SymbolNode) ? first.unescaped.to_sym : nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def child_context(node, child, context, guard_ruby)
|
|
119
|
+
if node.is_a?(Prism::CallNode) && child.equal?(node.block) && child.is_a?(Prism::BlockNode)
|
|
120
|
+
instance_eval = context.in_instance_eval || INSTANCE_EVAL_NAMES.include?(node.name)
|
|
121
|
+
context = context.with(in_block: true, in_instance_eval: instance_eval)
|
|
122
|
+
end
|
|
123
|
+
return context if context.guarded
|
|
124
|
+
|
|
125
|
+
version_guard_live?(node, child, guard_ruby) ? context : context.with(guarded: true)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Whether `child` stays reportable under `node`'s condition. An `if` / `unless` arm is, when its guard is
|
|
129
|
+
# decided on the stated Ruby and selects it. Any other branch of a conditional whose condition reads
|
|
130
|
+
# `RUBY_VERSION` is not, because the stated version is the lowest one the project runs on and an
|
|
131
|
+
# undecided guard may deselect the call on it.
|
|
132
|
+
def version_guard_live?(node, child, guard_ruby)
|
|
133
|
+
case node
|
|
134
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
135
|
+
return true if child.equal?(node.predicate) || !reads_ruby_version?(node.predicate)
|
|
136
|
+
|
|
137
|
+
arm_live?(node, child, guard_ruby)
|
|
138
|
+
when Prism::CaseNode, Prism::CaseMatchNode, Prism::WhileNode, Prism::UntilNode
|
|
139
|
+
condition = node.predicate
|
|
140
|
+
condition.nil? || child.equal?(condition) || !reads_ruby_version?(condition)
|
|
141
|
+
when Prism::AndNode, Prism::OrNode
|
|
142
|
+
child.equal?(node.left) || !reads_ruby_version?(node.left)
|
|
143
|
+
else
|
|
144
|
+
true
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def arm_live?(node, child, guard_ruby)
|
|
149
|
+
verdict = Inference::VersionGuard.verdict(node.predicate, stated_ruby: guard_ruby)
|
|
150
|
+
return false if verdict.nil?
|
|
151
|
+
|
|
152
|
+
then_live = verdict == (node.is_a?(Prism::IfNode) ? :truthy : :falsey)
|
|
153
|
+
child.equal?(node.statements) ? then_live : !then_live
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# `return if RUBY_VERSION < "2.7"` (or `raise` / `next` / `break` under such a guard): the statements
|
|
157
|
+
# after it run only on the Rubies the guard lets through.
|
|
158
|
+
def jumping_version_guard?(node)
|
|
159
|
+
return false unless node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
160
|
+
return false unless reads_ruby_version?(node.predicate)
|
|
161
|
+
|
|
162
|
+
other_arm = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
|
|
163
|
+
contains_jump?(node.statements) || contains_jump?(other_arm)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
JUMP_CALLS = %i[raise fail exit exit! abort].freeze
|
|
167
|
+
private_constant :JUMP_CALLS
|
|
168
|
+
|
|
169
|
+
def contains_jump?(node)
|
|
170
|
+
return false unless node.is_a?(Prism::Node)
|
|
171
|
+
|
|
172
|
+
case node
|
|
173
|
+
when Prism::ReturnNode, Prism::NextNode, Prism::BreakNode then return true
|
|
174
|
+
when Prism::CallNode then return true if node.receiver.nil? && JUMP_CALLS.include?(node.name)
|
|
175
|
+
end
|
|
176
|
+
found = false
|
|
177
|
+
node.rigor_each_child { |child| found ||= contains_jump?(child) }
|
|
178
|
+
found
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def reads_ruby_version?(node)
|
|
182
|
+
return false unless node.is_a?(Prism::Node)
|
|
183
|
+
return true if (node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)) &&
|
|
184
|
+
Source::ConstantPath.qualified_name_or_nil(node) == "RUBY_VERSION"
|
|
185
|
+
|
|
186
|
+
found = false
|
|
187
|
+
node.rigor_each_child { |child| found ||= reads_ruby_version?(child) }
|
|
188
|
+
found
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @return `[display, removal]` for a call that reaches a deprecated method, or nil
|
|
192
|
+
def deprecated_form(call, context, scope_index, shadowed)
|
|
193
|
+
scope = scope_index[call]
|
|
194
|
+
return nil if scope.nil?
|
|
195
|
+
|
|
196
|
+
via_send = SEND_NAMES.include?(call.name)
|
|
197
|
+
name = via_send ? send_target(call) : call.name
|
|
198
|
+
shadowed[name] = project_defines?(scope, name) unless shadowed.key?(name)
|
|
199
|
+
return nil if shadowed[name]
|
|
200
|
+
|
|
201
|
+
receiver = call.receiver
|
|
202
|
+
if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
203
|
+
self_form(name, scope, context)
|
|
204
|
+
else
|
|
205
|
+
receiver_form(name, scope.type_of(receiver), scope, via_send)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def self_form(name, scope, context)
|
|
210
|
+
self_type = scope.self_type
|
|
211
|
+
if self_type.nil?
|
|
212
|
+
return nil if context.in_block || name != :ruby2_keywords
|
|
213
|
+
|
|
214
|
+
return TOPLEVEL_FORM
|
|
215
|
+
end
|
|
216
|
+
return nil if context.in_instance_eval || !self_type.is_a?(Type::Singleton)
|
|
217
|
+
|
|
218
|
+
name == :ruby2_keywords ? MODULE_FORM : hash_form(name, self_type, scope)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def receiver_form(name, type, scope, via_send)
|
|
222
|
+
if name == :ruby2_keywords
|
|
223
|
+
return PROC_FORM if type.is_a?(Type::Nominal) && class_at_or_below?(type.class_name, "Proc", scope)
|
|
224
|
+
|
|
225
|
+
# `Module#ruby2_keywords` is private: only `send` reaches it on an explicit receiver.
|
|
226
|
+
return via_send && type.is_a?(Type::Singleton) ? MODULE_FORM : nil
|
|
227
|
+
end
|
|
228
|
+
type.is_a?(Type::Singleton) ? hash_form(name, type, scope) : nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def hash_form(name, singleton, scope)
|
|
232
|
+
return nil unless HASH_FORMS.key?(name)
|
|
233
|
+
|
|
234
|
+
class_at_or_below?(singleton.class_name, "Hash", scope) ? HASH_FORMS.fetch(name) : nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def class_at_or_below?(class_name, ancestor, scope)
|
|
238
|
+
return true if class_name == ancestor
|
|
239
|
+
return false if scope.environment.nil?
|
|
240
|
+
|
|
241
|
+
%i[equal subclass].include?(Rigor::Reflection.class_ordering(class_name, ancestor, scope: scope))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Whether a project or `pre_eval:` file defines a method called `name` anywhere — on any class, on either
|
|
245
|
+
# side, or at the top level. Asked once per name per file.
|
|
246
|
+
def project_defines?(scope, name)
|
|
247
|
+
return true if scope.discovered_methods.any? { |_owner, table| table.key?(name) }
|
|
248
|
+
return true if scope.discovered_def_nodes.any? { |_owner, table| table.key?(name) }
|
|
249
|
+
|
|
250
|
+
registry = scope.environment&.project_patched_methods
|
|
251
|
+
return false if registry.nil? || registry.empty?
|
|
252
|
+
|
|
253
|
+
registry.by_key.each_key.any? { |(_owner, method_name, _kind)| method_name == name }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def build(path, call, form, stated_ruby)
|
|
257
|
+
display, removal = form
|
|
258
|
+
tail = if ::Gem::Version.new(stated_ruby) >= ::Gem::Version.new(removal)
|
|
259
|
+
"was removed in Ruby #{removal}"
|
|
260
|
+
else
|
|
261
|
+
"is deprecated since Ruby 4.1 and will be removed in Ruby #{removal}"
|
|
262
|
+
end
|
|
263
|
+
Diagnostic.from_message_loc(
|
|
264
|
+
call,
|
|
265
|
+
rule: RULE_DEPRECATED_RUBY2_KEYWORDS,
|
|
266
|
+
path: path,
|
|
267
|
+
message: "`#{display}' #{tail} (target_ruby: #{stated_ruby.inspect})",
|
|
268
|
+
severity: :warning,
|
|
269
|
+
method_name: display
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -78,6 +78,9 @@ module Rigor
|
|
|
78
78
|
# (`SpecialGlobalSetters`), not the global's RBS declaration.
|
|
79
79
|
RULE_GLOBAL_WRITE_TYPE_MISMATCH = "global.write-type-mismatch"
|
|
80
80
|
RULE_GLOBAL_READONLY_WRITE = "global.readonly-write"
|
|
81
|
+
# Issue #1692 (the ADR-47 WD5 amendment) — a call to the `ruby2_keywords` family Ruby 4.1 deprecates. Runs only
|
|
82
|
+
# under an explicit `target_ruby` of 4.1 or later; see {RubyDeprecations}.
|
|
83
|
+
RULE_DEPRECATED_RUBY2_KEYWORDS = "call.deprecated-ruby2-keywords"
|
|
81
84
|
|
|
82
85
|
ALL_RULES = [
|
|
83
86
|
RULE_UNDEFINED_METHOD,
|
|
@@ -87,6 +90,7 @@ module Rigor
|
|
|
87
90
|
RULE_ARGUMENT_TYPE,
|
|
88
91
|
RULE_NIL_RECEIVER,
|
|
89
92
|
RULE_RAISE_NON_EXCEPTION,
|
|
93
|
+
RULE_DEPRECATED_RUBY2_KEYWORDS,
|
|
90
94
|
RULE_DUMP_TYPE,
|
|
91
95
|
RULE_ASSERT_TYPE,
|
|
92
96
|
RULE_ALWAYS_RAISES,
|
|
@@ -290,8 +290,11 @@ module Rigor
|
|
|
290
290
|
bucket[raw] = @scope.ancestor_name_candidates(owner, raw).find { |name| project_class?(name) }
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
+
# A bucket of class marks alone (issue #1820: `x.singleton_class.prepend(Comparable)`) does not make a name
|
|
294
|
+
# the project's.
|
|
293
295
|
def project_class?(name)
|
|
294
|
-
@scope.known_user_class?(name) ||
|
|
296
|
+
@scope.known_user_class?(name) ||
|
|
297
|
+
Scope::DiscoveryIndex.declares_surface?(@scope.discovered_parameter_envelopes[name])
|
|
295
298
|
end
|
|
296
299
|
|
|
297
300
|
# A module's methods reach the receiver as instance methods whichever side the receiver is on.
|
|
@@ -306,8 +309,13 @@ module Rigor
|
|
|
306
309
|
level.externals.all? { |candidates| external_mixin_lacks_method?(candidates) }
|
|
307
310
|
end
|
|
308
311
|
|
|
312
|
+
# Issue #1797 — a class whose body rewrites its method table in a way no literal names. A `refine` of the
|
|
313
|
+
# class (`ENVELOPE_REFINED_MARK`) is not one here: whether a refinement answers a call is decided per call
|
|
314
|
+
# site, name and `using` by `CheckRules#refined_redefinition_in_effect?`, which also declines where a
|
|
315
|
+
# refinement of a subclass of the receiver's class may answer. Counting the mark silenced every name on
|
|
316
|
+
# every subclass of a refined class, with or without a `using`.
|
|
309
317
|
def dynamic_surface?(class_name)
|
|
310
|
-
|
|
318
|
+
@scope.parameter_envelopes_of(class_name).key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
311
319
|
end
|
|
312
320
|
|
|
313
321
|
def project_patched?(class_name)
|
|
@@ -401,8 +409,12 @@ module Rigor
|
|
|
401
409
|
|
|
402
410
|
def first_level(class_name) = chain_levels(arity_chain(class_name)).first.first
|
|
403
411
|
|
|
412
|
+
# ADR-46 — every class whose subclasses are enumerated records `subclass_of:<last segment>`, which a class
|
|
413
|
+
# declaration with that superclass appearing or vanishing fires (issue #1820): a new subclass with a
|
|
414
|
+
# different envelope re-checks the call.
|
|
404
415
|
def each_subclass(class_name)
|
|
405
416
|
children = children_by_parent
|
|
417
|
+
record_subclasses_read(class_name)
|
|
406
418
|
queue = children.fetch(class_name.to_s, []).dup
|
|
407
419
|
seen = {}
|
|
408
420
|
until queue.empty?
|
|
@@ -410,11 +422,18 @@ module Rigor
|
|
|
410
422
|
next if seen[subclass]
|
|
411
423
|
|
|
412
424
|
seen[subclass] = true
|
|
425
|
+
record_subclasses_read(subclass)
|
|
413
426
|
yield subclass
|
|
414
427
|
queue.concat(children.fetch(subclass, []))
|
|
415
428
|
end
|
|
416
429
|
end
|
|
417
430
|
|
|
431
|
+
def record_subclasses_read(class_name)
|
|
432
|
+
return unless Analysis::DependencyRecorder.active?
|
|
433
|
+
|
|
434
|
+
Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name.to_s)
|
|
435
|
+
end
|
|
436
|
+
|
|
418
437
|
def children_by_parent
|
|
419
438
|
@scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
|
|
420
439
|
parent = resolve(child, raw_parent)
|