rigortype 0.4.0 → 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 +4 -3
- 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/08-understanding-errors.md +3 -2
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +6 -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/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- 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 +184 -61
- data/lib/rigor/analysis/check_rules.rb +407 -144
- data/lib/rigor/analysis/dependency_recorder.rb +215 -18
- 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 +291 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
- data/lib/rigor/analysis/incremental_session.rb +287 -50
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +43 -9
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +371 -49
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +11 -7
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +92 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -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 +85 -14
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- 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 +16 -10
- data/lib/rigor/cli.rb +11 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +11 -0
- data/lib/rigor/configuration.rb +52 -7
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +284 -15
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +34 -73
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +351 -61
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +54 -13
- 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 +73 -14
- 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 +305 -54
- 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 +214 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +166 -22
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- 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/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +2129 -354
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +149 -32
- 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/base.rb +28 -8
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +124 -7
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +4 -4
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +211 -46
- data/lib/rigor/reflection/constant_path.rb +33 -7
- data/lib/rigor/reflection.rb +41 -10
- data/lib/rigor/scope/discovery_index.rb +258 -3
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +1032 -0
- data/lib/rigor/scope.rb +480 -328
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +77 -21
- 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 +59 -3
- 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 +13 -2
- 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/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/scope.rbs +69 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +90 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# rigor-typelizer
|
|
2
|
+
|
|
3
|
+
Teaches `rigor unused` about the serializer classes the [typelizer](https://github.com/skryukov/typelizer) gem
|
|
4
|
+
generates TypeScript types from. typelizer writes an interface for every named class that does
|
|
5
|
+
`include Typelizer::DSL` or `extend Typelizer::DSL`, and for all of its subclasses. Such a class may never be
|
|
6
|
+
referenced from Ruby and still be used by your frontend through the generated type, so without this plugin
|
|
7
|
+
`rigor unused` lists it as a removal candidate. The plugin adds no diagnostic and no return type.
|
|
8
|
+
|
|
9
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
plugins:
|
|
13
|
+
- rigor-typelizer
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it roots
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# app/serializers/application_serializer.rb
|
|
20
|
+
class ApplicationSerializer
|
|
21
|
+
include Typelizer::DSL # rooted
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# app/serializers/user_serializer.rb
|
|
25
|
+
class UserSerializer < ApplicationSerializer # rooted: a subclass of a DSL class
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# app/serializers/event_serializer.rb
|
|
29
|
+
class EventSerializer
|
|
30
|
+
extend Typelizer::DSL # rooted
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class PlainFormatter # not rooted: no Typelizer::DSL on its chain
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A subclass is followed through any chain of project classes, and a compact header such as
|
|
38
|
+
`class Admin::UserSerializer < Base` resolves `Base` the way Ruby does (against the lexical scope, not
|
|
39
|
+
`Admin`).
|
|
40
|
+
|
|
41
|
+
## What it does not root
|
|
42
|
+
|
|
43
|
+
- A class outside the configured `dirs`. typelizer generates any DSL class that is loaded, but it loads only `dirs`;
|
|
44
|
+
a class elsewhere is loaded when something references it, so it is not dead.
|
|
45
|
+
- A `module` that includes `Typelizer::DSL`. typelizer registers the module's own name and then calls
|
|
46
|
+
`.descendants` on it, which a plain module does not have, so it generates no interface.
|
|
47
|
+
- A DSL call that does not run with the class as `self`: inside a method, a block, a lambda or `class << self`.
|
|
48
|
+
- A class whose DSL comes only from an `ActiveSupport::Concern` `included do include Typelizer::DSL end` block.
|
|
49
|
+
typelizer does register such a class; the plugin does not follow module hooks, so this is a missed root and the
|
|
50
|
+
class stays a candidate.
|
|
51
|
+
|
|
52
|
+
`reject_class`, the lambda typelizer's configuration uses to drop a class from the output, is evaluated at
|
|
53
|
+
runtime and is not modelled. A rejected class is still rooted, which hides a candidate and never invents a
|
|
54
|
+
finding.
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
plugins:
|
|
60
|
+
- gem: rigor-typelizer
|
|
61
|
+
config:
|
|
62
|
+
dirs: ["app/resources", "app/serializers"] # default
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`dirs` mirrors `Typelizer.dirs`; the default is what `Typelizer::Railtie` sets when you configure none. Set it
|
|
66
|
+
to the same list you give typelizer. Superclasses are resolved over all of your project's `paths:`, so a class
|
|
67
|
+
that shadows a base outside `dirs` is noticed.
|
|
68
|
+
|
|
69
|
+
## More
|
|
70
|
+
|
|
71
|
+
Internals and the known limits: [plugins/rigor-typelizer/README.md](../../../plugins/rigor-typelizer/README.md).
|
|
@@ -40,8 +40,10 @@ module Rigor
|
|
|
40
40
|
NODE_CLASSES = [Prism::DefNode].freeze
|
|
41
41
|
RULE_WALK_GATES = %i[inside_def detached_ivar_facet].freeze
|
|
42
42
|
|
|
43
|
-
# Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:}>]]`.
|
|
44
|
-
# when the tree has no qualifying writes.
|
|
43
|
+
# Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:, declared:}>]]`.
|
|
44
|
+
# Empty when the tree has no qualifying writes. `declared` is the translated type of the class's RBS
|
|
45
|
+
# declaration of the ivar — an `@x: T` member or the slot an `attr_*` member implies, own or
|
|
46
|
+
# inherited — and nil when the RBS declares none.
|
|
45
47
|
def initialize(scope_index)
|
|
46
48
|
@scope_index = scope_index
|
|
47
49
|
@accumulator = {}
|
|
@@ -112,9 +114,31 @@ module Rigor
|
|
|
112
114
|
return if scope.nil?
|
|
113
115
|
|
|
114
116
|
rvalue_type = scope.type_of(node.value)
|
|
117
|
+
declared = declared_ivar(scope, class_name, node.name)
|
|
115
118
|
@accumulator[class_name] ||= {}
|
|
116
119
|
@accumulator[class_name][node.name] ||= []
|
|
117
|
-
@accumulator[class_name][node.name] << { node: node, type: rvalue_type }
|
|
120
|
+
@accumulator[class_name][node.name] << { node: node, type: rvalue_type, declared: declared }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def declared_ivar(scope, class_name, ivar_name)
|
|
124
|
+
loader = scope.environment&.rbs_loader
|
|
125
|
+
return nil if loader.nil?
|
|
126
|
+
|
|
127
|
+
variable = loader.instance_definition(class_name)&.instance_variables&.[](ivar_name)
|
|
128
|
+
return nil if variable.nil?
|
|
129
|
+
|
|
130
|
+
translate_declared(variable.type, loader, class_name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `self` / `instance` in a class's declaration is that class; a module's stays untyped, since its
|
|
134
|
+
# instances belong to includers no RBS type names. A translation failure reads as untyped, which only
|
|
135
|
+
# ever withholds a finding.
|
|
136
|
+
def translate_declared(rbs_type, loader, class_name)
|
|
137
|
+
owner = Type::Combinator.nominal_of(class_name) unless loader.rbs_module?(class_name)
|
|
138
|
+
Inference::RbsTypeTranslator.translate(rbs_type, self_type: owner, instance_type: owner,
|
|
139
|
+
alias_expander: loader)
|
|
140
|
+
rescue StandardError
|
|
141
|
+
Type::Combinator.untyped
|
|
118
142
|
end
|
|
119
143
|
end
|
|
120
144
|
end
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../../inference/scope_indexer"
|
|
6
|
-
require_relative "../../source/constant_path"
|
|
7
6
|
require_relative "../../source/node_children"
|
|
8
7
|
|
|
9
8
|
module Rigor
|
|
@@ -12,13 +11,9 @@ module Rigor
|
|
|
12
11
|
# Issue #1120 — the two ways one file makes a method callable at some sites and not at others, which
|
|
13
12
|
# `call.undefined-method` has to ask the file's syntax about because no project-wide table can answer:
|
|
14
13
|
#
|
|
15
|
-
# - **Refinements.**
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# each module refines is the project-wide `Scope#discovered_refinements`; this answers whether any of
|
|
19
|
-
# those modules is in effect at a call site. A `using` whose argument is not a constant
|
|
20
|
-
# (`using Module.new { refine … }`) names no module, so every refinement counts as in effect throughout
|
|
21
|
-
# its file. A `using` inside a `def` raises in Ruby and activates nothing.
|
|
14
|
+
# - **Refinements.** Issue #1673 — answered by the file's in-effect refinements ({#refinements}, the
|
|
15
|
+
# {Inference::InEffectRefinements} the typer reads too); the rules ask it for derived answers
|
|
16
|
+
# (`refinement_active?`, `any_at?`, `refinement_def?`).
|
|
22
17
|
# - **Singleton defs on locals.** `def o.m` defines `m` on the one object `o` holds. The local's type
|
|
23
18
|
# is not changed; `o.m` is simply not reported within the scope the `def` is written in (the
|
|
24
19
|
# enclosing `def`, `class` / `module` body, or file — a block shares its enclosing scope's locals).
|
|
@@ -26,42 +21,18 @@ module Rigor
|
|
|
26
21
|
# Built lazily: the walk runs the first time a would-fire call asks, so a file with no such call pays
|
|
27
22
|
# one small object and nothing else.
|
|
28
23
|
class LexicalMethodSites
|
|
29
|
-
|
|
24
|
+
# Issue #1703 — the file's root, which `call.possible-nil-receiver` re-walks for a `key?` guard.
|
|
25
|
+
attr_reader :root
|
|
26
|
+
|
|
27
|
+
# `refinements` is the query `Inference::ScopeIndexer` stamped on the file's discovery index, so the rules and
|
|
28
|
+
# the typer share one walk; a query over another tree, or none, builds this file's own on the first ask.
|
|
29
|
+
def initialize(root, refinements = nil)
|
|
30
30
|
@root = root
|
|
31
31
|
@built = false
|
|
32
|
+
@refinements = refinements if refinements&.over?(root)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
def refinement_active?(call_node, modules)
|
|
36
|
-
build
|
|
37
|
-
return true if @unresolved_using
|
|
38
|
-
|
|
39
|
-
offset = call_node.location.start_offset
|
|
40
|
-
return true if @refine_blocks.any? { |start, stop| offset >= start && offset < stop }
|
|
41
|
-
|
|
42
|
-
@usings.any? do |start, stop, candidates|
|
|
43
|
-
offset >= start && offset < stop && candidates.any? { |name| modules.include?(name) }
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Issue #1367 — is any refinement in effect at `node`: a `using` of any module, a `using` of a non-constant, or
|
|
48
|
-
# the inside of a `refine` block? The `global.*` stream check asks it where it cannot name the refining
|
|
49
|
-
# module (a `define_method` or `alias_method` inside `refine` records none).
|
|
50
|
-
def using_in_effect?(node)
|
|
51
|
-
build
|
|
52
|
-
return true if @unresolved_using
|
|
53
|
-
|
|
54
|
-
offset = node.location.start_offset
|
|
55
|
-
@refine_blocks.any? { |start, stop| offset >= start && offset < stop } ||
|
|
56
|
-
@usings.any? { |start, stop, _candidates| offset >= start && offset < stop }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Is `def_node` one of the defs a `refine X do … end` body defines on X? Such a def redefines X's method by
|
|
60
|
-
# design, so X's declared signature for the name is not its contract (issue #1120, maintainer ruling a′).
|
|
61
|
-
def refinement_def?(def_node)
|
|
62
|
-
build
|
|
63
|
-
@refinement_defs.include?(def_node.location.start_offset)
|
|
64
|
-
end
|
|
35
|
+
def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
|
|
65
36
|
|
|
66
37
|
# Does a `def <local>.<name>` for this call's local receiver and method name sit in the scope the call
|
|
67
38
|
# is in?
|
|
@@ -92,45 +63,35 @@ module Rigor
|
|
|
92
63
|
return if @built
|
|
93
64
|
|
|
94
65
|
@built = true
|
|
95
|
-
@usings = []
|
|
96
|
-
@refine_blocks = []
|
|
97
66
|
@singleton_defs = []
|
|
98
67
|
@scopes = []
|
|
99
|
-
@refinement_defs = Set.new
|
|
100
|
-
@unresolved_using = false
|
|
101
68
|
return if @root.nil?
|
|
102
69
|
|
|
103
70
|
location = @root.location
|
|
104
71
|
span = [location.start_offset, location.end_offset]
|
|
105
|
-
walk(@root,
|
|
72
|
+
walk(@root, span)
|
|
106
73
|
end
|
|
107
74
|
|
|
108
|
-
# `
|
|
109
|
-
|
|
110
|
-
def walk(node, prefix, body, locals, in_def)
|
|
75
|
+
# `locals` is the span of the scope a local written here belongs to.
|
|
76
|
+
def walk(node, locals)
|
|
111
77
|
case node
|
|
112
78
|
when Prism::ClassNode, Prism::ModuleNode
|
|
113
|
-
|
|
114
|
-
inner = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
|
|
115
|
-
return walk_children(node.body, inner, span, span, false)
|
|
79
|
+
return walk_children(node.body, scope_span(node))
|
|
116
80
|
when Prism::SingletonClassNode
|
|
117
|
-
walk(node.expression,
|
|
118
|
-
|
|
119
|
-
return walk_children(node.body, prefix, span, span, false)
|
|
81
|
+
walk(node.expression, locals)
|
|
82
|
+
return walk_children(node.body, scope_span(node))
|
|
120
83
|
when Prism::DefNode
|
|
121
84
|
record_singleton_def(node, locals)
|
|
122
|
-
return walk_children(node.body,
|
|
123
|
-
when Prism::CallNode
|
|
124
|
-
record_call(node, prefix, body, in_def)
|
|
85
|
+
return walk_children(node.body, scope_span(node))
|
|
125
86
|
end
|
|
126
87
|
|
|
127
|
-
walk_children(node,
|
|
88
|
+
walk_children(node, locals)
|
|
128
89
|
end
|
|
129
90
|
|
|
130
|
-
def walk_children(node,
|
|
91
|
+
def walk_children(node, locals)
|
|
131
92
|
return if node.nil?
|
|
132
93
|
|
|
133
|
-
node.rigor_each_child { |child| walk(child,
|
|
94
|
+
node.rigor_each_child { |child| walk(child, locals) }
|
|
134
95
|
end
|
|
135
96
|
|
|
136
97
|
def record_singleton_def(node, locals)
|
|
@@ -140,38 +101,6 @@ module Rigor
|
|
|
140
101
|
@singleton_defs << [receiver.name, node.name, *locals]
|
|
141
102
|
end
|
|
142
103
|
|
|
143
|
-
def record_call(node, prefix, body, in_def)
|
|
144
|
-
if Inference::ScopeIndexer.refine_target(node)
|
|
145
|
-
@refine_blocks << span_of(node.block)
|
|
146
|
-
record_refinement_defs(node.block.body)
|
|
147
|
-
elsif using_call?(node) && !in_def
|
|
148
|
-
record_using(node, prefix, body)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def record_refinement_defs(body)
|
|
153
|
-
return if body.nil?
|
|
154
|
-
|
|
155
|
-
Inference::ScopeIndexer.each_refinement_def(body) do |def_node|
|
|
156
|
-
@refinement_defs << def_node.location.start_offset
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def using_call?(node)
|
|
161
|
-
node.name == :using && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
162
|
-
node.arguments&.arguments&.size == 1
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def record_using(node, prefix, body)
|
|
166
|
-
argument = node.arguments.arguments.first
|
|
167
|
-
if argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode)
|
|
168
|
-
candidates = Inference::ScopeIndexer.constant_receiver_candidates(argument, prefix)
|
|
169
|
-
@usings << [node.location.end_offset, body.last, candidates]
|
|
170
|
-
else
|
|
171
|
-
@unresolved_using = true
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
104
|
# The span of a node that opens a local scope, recorded for {#nested_scope_holds?}.
|
|
176
105
|
def scope_span(node)
|
|
177
106
|
span = span_of(node)
|
|
@@ -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,
|