rigortype 0.4.3 → 0.4.5
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 +5 -5
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +56 -8
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -29,6 +29,7 @@ require_relative "check_rules/void_value_use_collector"
|
|
|
29
29
|
require_relative "check_rules/self_closedness_scanner"
|
|
30
30
|
require_relative "check_rules/source_arity"
|
|
31
31
|
require_relative "check_rules/lexical_method_sites"
|
|
32
|
+
require_relative "check_rules/rbs_receiver_surface"
|
|
32
33
|
require_relative "check_rules/special_global_setters"
|
|
33
34
|
require_relative "check_rules/ruby_deprecations"
|
|
34
35
|
|
|
@@ -79,19 +80,8 @@ module Rigor
|
|
|
79
80
|
# runtime one and `def.override-visibility-reduced` cannot prove a reduction.
|
|
80
81
|
IMPLICITLY_PRIVATE_METHODS = (LIFECYCLE_HOOKS + %i[respond_to_missing?]).freeze
|
|
81
82
|
|
|
82
|
-
#
|
|
83
|
-
# `
|
|
84
|
-
# to the set of canonical rule identifiers it disables.
|
|
85
|
-
# Returns `nil` for `"all"` (the existing wildcard meaning
|
|
86
|
-
# "every rule"), or for unknown tokens.
|
|
87
|
-
def self.resolve_rule_token(token)
|
|
88
|
-
return nil if token == "all"
|
|
89
|
-
return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
|
|
90
|
-
return PRE_EVAL_RULES if token == "pre-eval"
|
|
91
|
-
return ALL_RULES.select { |r| r.start_with?("#{token}.") } if RULE_FAMILIES.include?(token)
|
|
92
|
-
|
|
93
|
-
ALL_RULES.include?(token) ? [token] : []
|
|
94
|
-
end
|
|
83
|
+
# The token expansion (`resolve_rule_token` / `expand_rule_tokens` / `expand_token`) lives in the light
|
|
84
|
+
# `check_rules/rule_ids.rb`, so {SeverityStamp} can apply the `disable:` list on the engine-free hit path.
|
|
95
85
|
|
|
96
86
|
module_function
|
|
97
87
|
|
|
@@ -480,7 +470,7 @@ module Rigor
|
|
|
480
470
|
# - **File-level** (v0.1.2): `# rigor:disable-file <rule1>,
|
|
481
471
|
# <rule2>` anywhere in the file suppresses the matching
|
|
482
472
|
# diagnostic for every line. `# rigor:disable-file all`
|
|
483
|
-
# suppresses every rule across the file. Convention is
|
|
473
|
+
# suppresses every built-in rule across the file. Convention is
|
|
484
474
|
# to put the comment near the top, but Rigor accepts it
|
|
485
475
|
# anywhere — the comment scope is "this file" regardless
|
|
486
476
|
# of position.
|
|
@@ -488,7 +478,8 @@ module Rigor
|
|
|
488
478
|
# on the same line as the offending expression
|
|
489
479
|
# suppresses the matching diagnostic for that line
|
|
490
480
|
# only. `# rigor:disable all` on a line suppresses
|
|
491
|
-
# every rule on that line.
|
|
481
|
+
# every built-in rule on that line. A plugin's rows are
|
|
482
|
+
# not passed here; see {.filter_plugin_suppressed}.
|
|
492
483
|
#
|
|
493
484
|
# Both in-source forms are recognised only when the
|
|
494
485
|
# marker is the FIRST thing in the comment, so a
|
|
@@ -507,7 +498,7 @@ module Rigor
|
|
|
507
498
|
diagnostics.reject do |diagnostic|
|
|
508
499
|
rule = diagnostic.rule
|
|
509
500
|
next false if rule.nil?
|
|
510
|
-
next true if
|
|
501
|
+
next true if config_disabled?(rule, disabled)
|
|
511
502
|
next true if file_suppressions.include?("all") || file_suppressions.include?(rule)
|
|
512
503
|
|
|
513
504
|
line_rules = line_suppressions[diagnostic.line]
|
|
@@ -515,6 +506,26 @@ module Rigor
|
|
|
515
506
|
end
|
|
516
507
|
end
|
|
517
508
|
|
|
509
|
+
# Issue #1953 — the in-source markers over the rows a plugin emitted while `path` was analysed
|
|
510
|
+
# (`#diagnostics_for_file` and node rules). A row answers to a `plugin.<id>.<rule>` or `plugin.<id>` token
|
|
511
|
+
# only ({.plugin_row_disabled?}); a bare id and `all` keep reaching built-in rows alone. `comments` are
|
|
512
|
+
# `path`'s, so a row the plugin positioned in another file is left to the `disable:` list and the baseline.
|
|
513
|
+
# The `disable:` list itself is applied to these rows by {SeverityStamp}, on every run shape.
|
|
514
|
+
def filter_plugin_suppressed(diagnostics, path:, comments:)
|
|
515
|
+
return diagnostics if diagnostics.empty?
|
|
516
|
+
|
|
517
|
+
line_suppressions, file_suppressions = parse_suppression_comments(comments)
|
|
518
|
+
return diagnostics if line_suppressions.empty? && file_suppressions.empty?
|
|
519
|
+
|
|
520
|
+
diagnostics.reject do |diagnostic|
|
|
521
|
+
next false unless diagnostic.path == path
|
|
522
|
+
next true if plugin_row_disabled?(diagnostic, file_suppressions)
|
|
523
|
+
|
|
524
|
+
line_rules = line_suppressions.fetch(diagnostic.line, nil)
|
|
525
|
+
!line_rules.nil? && plugin_row_disabled?(diagnostic, line_rules)
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
518
529
|
# Every suppression-recognition pattern below is anchored with `\A` against the COMMENT SLICE —
|
|
519
530
|
# Prism hands us a slice that starts at the `#`, so `\A#` means "the marker is the first thing in
|
|
520
531
|
# the comment". A real directive always has that shape, in both the whole-line form and the
|
|
@@ -704,24 +715,6 @@ module Rigor
|
|
|
704
715
|
)
|
|
705
716
|
end
|
|
706
717
|
|
|
707
|
-
# Expands a list of user-supplied rule tokens into the
|
|
708
|
-
# canonical-id set per ADR-8 § "Backward compatibility".
|
|
709
|
-
# `disabled_rules` accepts unprefixed legacy names
|
|
710
|
-
# (`undefined-method`), canonical names
|
|
711
|
-
# (`call.undefined-method`), and family wildcards (`call`).
|
|
712
|
-
def expand_rule_tokens(tokens)
|
|
713
|
-
Array(tokens).each_with_object(Set.new) do |token, set|
|
|
714
|
-
set.merge(expand_token(token.to_s))
|
|
715
|
-
end
|
|
716
|
-
end
|
|
717
|
-
|
|
718
|
-
def expand_token(token)
|
|
719
|
-
return ["all"] if token == "all"
|
|
720
|
-
|
|
721
|
-
resolved = resolve_rule_token(token)
|
|
722
|
-
resolved.nil? || resolved.empty? ? [token] : resolved
|
|
723
|
-
end
|
|
724
|
-
|
|
725
718
|
# rubocop:disable-next Metrics/ClassLength
|
|
726
719
|
class << self
|
|
727
720
|
private
|
|
@@ -809,7 +802,7 @@ module Rigor
|
|
|
809
802
|
# rule must not surface a false `undefined-method`
|
|
810
803
|
# for them.
|
|
811
804
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
812
|
-
return nil if source_declared_method?(scope, class_name, call_node.name, kind)
|
|
805
|
+
return nil if source_declared_method?(scope, class_name, call_node.name, kind, call_node: call_node)
|
|
813
806
|
|
|
814
807
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
815
808
|
|
|
@@ -825,6 +818,8 @@ module Rigor
|
|
|
825
818
|
return nil if method_def
|
|
826
819
|
|
|
827
820
|
return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites)
|
|
821
|
+
return nil if subclass_instance_may_answer?(rendered_receiver, class_name, call_node.name, scope,
|
|
822
|
+
call_node.receiver)
|
|
828
823
|
|
|
829
824
|
definition_site = project_definition_site(scope, class_name, call_node.name, kind)
|
|
830
825
|
# Issue #735 — the site is evidence, and whose RBS this is decides what it is evidence OF. On a
|
|
@@ -959,12 +954,51 @@ module Rigor
|
|
|
959
954
|
# (see {#ancestry_declares_method?} for why that placement is load-bearing, not tidiness).
|
|
960
955
|
def last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites = nil)
|
|
961
956
|
return true if lexically_defined_method?(class_name, call_node, scope, kind, lexical_sites)
|
|
957
|
+
return true if project_patched_through_ancestors?(scope, class_name, call_node.name, kind,
|
|
958
|
+
call_node: call_node)
|
|
962
959
|
return true if module_mixin_receiver?(receiver_type, scope)
|
|
963
960
|
return true if mixin_self_class_receiver?(call_node, scope)
|
|
964
961
|
return true if unknown_mixin_includer?(class_name, scope)
|
|
965
962
|
return true if object_mixin_may_define?(receiver_type, class_name, call_node.name, scope)
|
|
963
|
+
return true if ancestry_declares_method?(scope, class_name, call_node.name, kind)
|
|
966
964
|
|
|
967
|
-
|
|
965
|
+
rbs_ancestor_may_answer?(class_name, call_node.name, scope, kind)
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
# Issue #1722 — a `method_missing`, or a project mixin, on one of the class's RBS ancestors
|
|
969
|
+
# ({RbsReceiverSurface}). Last for {#ancestry_declares_method?}'s reason: it walks the class graph.
|
|
970
|
+
def rbs_ancestor_may_answer?(class_name, method_name, scope, kind)
|
|
971
|
+
if kind == :singleton
|
|
972
|
+
RbsReceiverSurface.singleton_hook?(scope, class_name)
|
|
973
|
+
else
|
|
974
|
+
RbsReceiverSurface.ancestor_may_answer?(scope, class_name, method_name)
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Issue #1722 — a receiver typed by its class's name, rather than by a literal, may hold an instance of a
|
|
979
|
+
# subclass (a `#: () -> String` signature admits a `MyStr < String`), so a method a project subclass
|
|
980
|
+
# defines, or a subclass's `method_missing`, may be the one that runs. A value the engine built from a
|
|
981
|
+
# literal is exactly its class and keeps firing: a literal type (`"a"`, `1`, a union of them), a shape
|
|
982
|
+
# carrier (`{a: 1}`, `[1, 2]`, `{}`, `[]`, and what folds to one), or a receiver written as a literal
|
|
983
|
+
# (`"a#{x}b"`, which types as plain `String`). A local holding an interpolated String types as plain
|
|
984
|
+
# `String` with no origin, so it still declines. Instance side only.
|
|
985
|
+
def subclass_instance_may_answer?(receiver_type, class_name, method_name, scope, receiver_node = nil)
|
|
986
|
+
return false if receiver_type.is_a?(Type::Singleton)
|
|
987
|
+
return false if exact_literal_receiver?(receiver_type) || LITERAL_RECEIVER_NODES.include?(receiver_node.class)
|
|
988
|
+
|
|
989
|
+
RbsReceiverSurface.subclass_may_define?(scope, class_name, method_name)
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
EXACT_TYPES = [Type::Constant, Type::Tuple, Type::HashShape].freeze
|
|
993
|
+
LITERAL_RECEIVER_NODES = [
|
|
994
|
+
Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode, Prism::InterpolatedXStringNode,
|
|
995
|
+
Prism::ArrayNode, Prism::HashNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
|
|
996
|
+
].to_set.freeze
|
|
997
|
+
private_constant :EXACT_TYPES, :LITERAL_RECEIVER_NODES
|
|
998
|
+
|
|
999
|
+
def exact_literal_receiver?(receiver_type)
|
|
1000
|
+
members = receiver_type.is_a?(Type::Union) ? receiver_type.members : [receiver_type]
|
|
1001
|
+
members.all? { |member| EXACT_TYPES.any? { |kind| member.is_a?(kind) } }
|
|
968
1002
|
end
|
|
969
1003
|
|
|
970
1004
|
# Issue #1697 — a module the project mixes into `Object` (a top-level `include M`, or `include M` in
|
|
@@ -1066,10 +1100,13 @@ module Rigor
|
|
|
1066
1100
|
# Both paths sit at the same dispatcher precedence; the
|
|
1067
1101
|
# check must hold them together so neither rule fires a
|
|
1068
1102
|
# false positive.
|
|
1069
|
-
|
|
1103
|
+
#
|
|
1104
|
+
# `call_node:` (issue #1708), when given, is the call asking: a private or protected `pre_eval:` patch
|
|
1105
|
+
# it may not reach does not count ({#patch_callable?}).
|
|
1106
|
+
def source_declared_method?(scope, class_name, method_name, kind, call_node: nil)
|
|
1070
1107
|
return true if scope.discovered_method?(class_name, method_name, kind)
|
|
1071
1108
|
|
|
1072
|
-
project_patched_method?(scope, class_name, method_name, kind)
|
|
1109
|
+
project_patched_method?(scope, class_name, method_name, kind, call_node: call_node)
|
|
1073
1110
|
end
|
|
1074
1111
|
|
|
1075
1112
|
# Issue #723 — the same question as {#source_declared_method?}, asked through the project's own
|
|
@@ -1095,12 +1132,42 @@ module Rigor
|
|
|
1095
1132
|
# inference (between plugins and dependency-source).
|
|
1096
1133
|
# Returns false when the environment carries no registry
|
|
1097
1134
|
# (legacy path) or the lookup misses.
|
|
1098
|
-
def project_patched_method?(scope, class_name, method_name, kind)
|
|
1135
|
+
def project_patched_method?(scope, class_name, method_name, kind, call_node: nil)
|
|
1099
1136
|
environment = scope.environment
|
|
1100
1137
|
registry = environment&.project_patched_methods
|
|
1101
1138
|
return false if registry.nil? || registry.empty?
|
|
1102
1139
|
|
|
1103
|
-
|
|
1140
|
+
entry = registry.lookup(class_name: class_name.to_s, method_name: method_name.to_sym, kind: kind)
|
|
1141
|
+
!entry.nil? && patch_callable?(registry, entry, scope, call_node)
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
# Issue #1708 — {#project_patched_method?} asked through the receiver's ancestors: a `pre_eval:` patch on
|
|
1145
|
+
# `Object`, `Numeric` or `Enumerable` gives an `Integer` or `Array` the method. Asked only once the
|
|
1146
|
+
# receiver's RBS has come back without the method, so a nearer class's own declaration still decides arity
|
|
1147
|
+
# and argument types; {#source_declared_method?} stays exact for the rules that read it for those.
|
|
1148
|
+
#
|
|
1149
|
+
# `call_node:` as {#source_declared_method?} reads it.
|
|
1150
|
+
def project_patched_through_ancestors?(scope, class_name, method_name, kind, call_node: nil)
|
|
1151
|
+
environment = scope.environment
|
|
1152
|
+
registry = environment&.project_patched_methods
|
|
1153
|
+
return false if registry.nil? || registry.empty?
|
|
1154
|
+
|
|
1155
|
+
entry = registry.lookup_through_ancestors(class_name: class_name.to_s, method_name: method_name.to_sym,
|
|
1156
|
+
kind: kind, rbs_loader: environment.rbs_loader)
|
|
1157
|
+
!entry.nil? && patch_callable?(registry, entry, scope, call_node)
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
# Issue #1708 — whether `call_node` may reach the patch `entry`: Ruby raises NoMethodError for a private
|
|
1161
|
+
# patch called with an explicit receiver other than `self` (`1.kp` on `module Kernel; private def kp`), and
|
|
1162
|
+
# for a protected one called from outside its owner's instances. No call node, and a declined verdict (another
|
|
1163
|
+
# definer the call may reach, {Inference::ProjectPatchedMethods#callable?}), are the permissive answer.
|
|
1164
|
+
def patch_callable?(registry, entry, scope, call_node)
|
|
1165
|
+
return true if call_node.nil?
|
|
1166
|
+
|
|
1167
|
+
receiver = call_node.receiver
|
|
1168
|
+
verdict = registry.callable?(entry, explicit: !receiver.nil? && !receiver.is_a?(Prism::SelfNode),
|
|
1169
|
+
self_type: scope.self_type, rbs_loader: scope.environment.rbs_loader)
|
|
1170
|
+
verdict != false
|
|
1104
1171
|
end
|
|
1105
1172
|
|
|
1106
1173
|
# ADR-34 — `call.unresolved-toplevel`. Fires on an
|
|
@@ -1149,6 +1216,8 @@ module Rigor
|
|
|
1149
1216
|
return nil if scope.top_level_def_for(name)
|
|
1150
1217
|
return nil if source_declared_method?(scope, "Object", name, :instance)
|
|
1151
1218
|
return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
|
|
1219
|
+
# Issue #1708 — `main` is an Object, so a `pre_eval:` patch on `Kernel` or `BasicObject` defines the name too.
|
|
1220
|
+
return nil if project_patched_through_ancestors?(scope, "Object", name, :instance)
|
|
1152
1221
|
return nil if main_singleton_method?(name, scope)
|
|
1153
1222
|
return nil if Inference::ObjectMixins.may_define?(scope, name, receiver: :implicit)
|
|
1154
1223
|
# `Target.class_eval { def added = 1; def use_added = added }` files the defs on Target
|
|
@@ -1393,6 +1462,21 @@ module Rigor
|
|
|
1393
1462
|
GEM_OVERLAY_OPEN_RECEIVERS = Set["ActiveSupport::Duration", "ActiveSupport::Concern"].freeze
|
|
1394
1463
|
private_constant :GEM_OVERLAY_OPEN_RECEIVERS
|
|
1395
1464
|
|
|
1465
|
+
# Issue #1456 — the `data/core_overlay/` twin of {GEM_OVERLAY_OPEN_RECEIVERS}. `OpenStruct` defines each field
|
|
1466
|
+
# as a per-instance singleton method and answers the rest through a private `method_missing` (ostruct 0.6), so
|
|
1467
|
+
# no signature can enumerate its surface. The only other declaration in the environment is json's add-on
|
|
1468
|
+
# reopening (`json_create` / `as_json` / `to_json`), which made the class read as fully declared and every
|
|
1469
|
+
# field access a `call.undefined-method`. `data/core_overlay/ostruct.rbs` declares that `method_missing`, and
|
|
1470
|
+
# an RBS-declared `method_missing` does not by itself open a receiver here, so the class is named. No load
|
|
1471
|
+
# gate is needed: the core overlay loads into every environment that has RBS at all, so the list is active
|
|
1472
|
+
# whenever the declaration it protects is. Both sides of the class are open, as with
|
|
1473
|
+
# {GEM_OVERLAY_OPEN_RECEIVERS}, and an inherited Object / Kernel signature stops deciding `call.wrong-arity`
|
|
1474
|
+
# and `call.argument-type-mismatch` ({#unauthoritative_inherited_signature?}): lost true positives,
|
|
1475
|
+
# accepted. The general rule — a class declared only by another library's partial reopening — is #1978, not
|
|
1476
|
+
# this list.
|
|
1477
|
+
CORE_OVERLAY_OPEN_RECEIVERS = Set["OpenStruct"].freeze
|
|
1478
|
+
private_constant :CORE_OVERLAY_OPEN_RECEIVERS
|
|
1479
|
+
|
|
1396
1480
|
# ADR-26 — whether `class_name` is declared "open" by a
|
|
1397
1481
|
# loaded plugin (manifest `open_receivers:`). An open
|
|
1398
1482
|
# class responds beyond its RBS surface, so the
|
|
@@ -1401,11 +1485,14 @@ module Rigor
|
|
|
1401
1485
|
# RBS method table, so proving a call "undefined" against it is
|
|
1402
1486
|
# unsound: a plugin-declared open receiver, a Rigor-bundled-
|
|
1403
1487
|
# overlay open receiver ({GEM_OVERLAY_OPEN_RECEIVERS}) whose
|
|
1404
|
-
# bundled directory this run actually loaded,
|
|
1488
|
+
# bundled directory this run actually loaded, a core-overlay
|
|
1489
|
+
# open receiver ({CORE_OVERLAY_OPEN_RECEIVERS}), or a
|
|
1405
1490
|
# Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`).
|
|
1406
1491
|
def unbounded_receiver_surface?(class_name, scope)
|
|
1492
|
+
bare_name = class_name.to_s.sub(/\A::/, "")
|
|
1407
1493
|
open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope) ||
|
|
1408
|
-
|
|
1494
|
+
CORE_OVERLAY_OPEN_RECEIVERS.include?(bare_name) ||
|
|
1495
|
+
(GEM_OVERLAY_OPEN_RECEIVERS.include?(bare_name) && gem_overlay_loaded?(scope))
|
|
1409
1496
|
end
|
|
1410
1497
|
|
|
1411
1498
|
def open_receiver?(class_name, scope)
|
|
@@ -2133,6 +2220,7 @@ module Rigor
|
|
|
2133
2220
|
return true unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
2134
2221
|
return true unless definition_available?(member, class_name, scope)
|
|
2135
2222
|
return true if lookup_method(member, class_name, method_name, scope)
|
|
2223
|
+
return true if project_patched_through_ancestors?(scope, class_name, method_name, :instance)
|
|
2136
2224
|
|
|
2137
2225
|
# Issue #723 — the arm's own RBS does not have it; its project ancestry still might. Last, for
|
|
2138
2226
|
# the reason {#ancestry_declares_method?} carries: this is the probe that walks the class graph.
|
|
@@ -2186,12 +2274,30 @@ module Rigor
|
|
|
2186
2274
|
# `compose_codepoints` typed `Hash | Hash` for an `Array`, flagging
|
|
2187
2275
|
# `.pack`). Require at least two distinct arm classes.
|
|
2188
2276
|
return nil if members.map { |member| concrete_class_name(member) }.uniq.size < 2
|
|
2189
|
-
return nil if
|
|
2190
|
-
return nil if refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2277
|
+
return nil if union_may_answer?(members, call_node, scope, lexical_sites)
|
|
2191
2278
|
|
|
2192
2279
|
build_undefined_method_diagnostic(path, call_node, receiver_type)
|
|
2193
2280
|
end
|
|
2194
2281
|
|
|
2282
|
+
# Whether some arm of a non-nil union of classes may answer the call, which keeps the union rule silent.
|
|
2283
|
+
def union_may_answer?(members, call_node, scope, lexical_sites)
|
|
2284
|
+
return true if members.any? { |member| method_present_anywhere?(member, call_node.name, scope) }
|
|
2285
|
+
return true if members.any? { |member| arm_surface_may_answer?(member, call_node.name, scope) }
|
|
2286
|
+
# Issue #1722 — `def o.m`, `o.extend(M)` and their kin give the one object the method, whatever its class.
|
|
2287
|
+
return true if lexical_sites&.singleton_local_def?(call_node)
|
|
2288
|
+
|
|
2289
|
+
refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2290
|
+
end
|
|
2291
|
+
|
|
2292
|
+
# Issue #1722 — the scalar rule's RBS-ancestor and subclass declines, asked of one arm. Kept out of
|
|
2293
|
+
# {#method_present_anywhere?}, which `call.possible-nil-receiver` also asks: there a "present" answer is
|
|
2294
|
+
# what lets the rule fire, and a `method_missing` on `Object` answers on the `nil` arm too.
|
|
2295
|
+
def arm_surface_may_answer?(member, method_name, scope)
|
|
2296
|
+
class_name = concrete_class_name(member)
|
|
2297
|
+
rbs_ancestor_may_answer?(class_name, method_name, scope, :instance) ||
|
|
2298
|
+
subclass_instance_may_answer?(member, class_name, method_name, scope)
|
|
2299
|
+
end
|
|
2300
|
+
|
|
2195
2301
|
# ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may define the name on one arm's class.
|
|
2196
2302
|
def refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2197
2303
|
members.any? do |member|
|
|
@@ -2855,6 +2961,13 @@ module Rigor
|
|
|
2855
2961
|
# - Issue #1568 — a module the class PREPENDS sits ahead of it in Ruby's order, so a definition there
|
|
2856
2962
|
# is what the call reaches; the class's own private `def` answers only when nothing prepended
|
|
2857
2963
|
# defines the name ({#reached_definer_visibility}).
|
|
2964
|
+
# - Issue #1984 — a class-object receiver (`Type::Singleton`, `K.x`) is checked against the singleton side:
|
|
2965
|
+
# the definition the call reaches on `K`'s singleton chain (its own singleton methods, its extended
|
|
2966
|
+
# modules' instance methods, then the superclass's singleton chain) must be recorded `:private`
|
|
2967
|
+
# (`private_class_method`, `class << self` with `private`, a private method of an extended module).
|
|
2968
|
+
# - Issue #1984 — Ruby hands a private call to `method_missing`, so a `method_missing` or
|
|
2969
|
+
# `respond_to_missing?` a project entry on the receiver's chain records, on the side the call reaches,
|
|
2970
|
+
# declines the rule ({#missing_hook_on_chain?}).
|
|
2858
2971
|
def visibility_mismatch_diagnostic(path, call_node, scope_index, lexical_sites = nil)
|
|
2859
2972
|
return nil unless explicit_non_self_receiver?(call_node.receiver)
|
|
2860
2973
|
|
|
@@ -2866,18 +2979,125 @@ module Rigor
|
|
|
2866
2979
|
return nil if inferred_param_receiver?(call_node, scope)
|
|
2867
2980
|
|
|
2868
2981
|
receiver_type = scope.type_of(call_node.receiver)
|
|
2869
|
-
|
|
2982
|
+
case receiver_type
|
|
2983
|
+
when Type::Nominal then instance_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
2984
|
+
when Type::Singleton then singleton_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
2985
|
+
end
|
|
2986
|
+
end
|
|
2870
2987
|
|
|
2988
|
+
def instance_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
2871
2989
|
visibility = scope.discovered_method_visibility(receiver_type.class_name, call_node.name)
|
|
2872
2990
|
return nil unless visibility == :private
|
|
2873
2991
|
|
|
2874
2992
|
reached = reached_definer_visibility(scope, receiver_type.class_name, call_node.name)
|
|
2875
2993
|
return nil if reached == :undecided
|
|
2876
2994
|
return nil unless reached.nil? || reached == :private
|
|
2995
|
+
return nil if missing_hook_on_chain?(scope, receiver_type.class_name, :instance)
|
|
2877
2996
|
# ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may redefine the method, public.
|
|
2878
2997
|
return nil if refined_method_in_effect?(receiver_type.class_name, call_node, scope, :instance, lexical_sites)
|
|
2879
2998
|
|
|
2880
|
-
build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
|
|
2999
|
+
build_visibility_mismatch_diagnostic(path, call_node, receiver_type.class_name)
|
|
3000
|
+
end
|
|
3001
|
+
|
|
3002
|
+
# Issue #1984 — the singleton side reads only the reached definer: a private singleton method may be inherited
|
|
3003
|
+
# (`private_class_method :new` on a superclass) or come from an extended module, so there is no own-table
|
|
3004
|
+
# gate as on the instance side. It reports only:
|
|
3005
|
+
#
|
|
3006
|
+
# - a constant receiver (`K.x`, `A::K.x`): `self.class.x` or a local may hold a subclass that makes the name
|
|
3007
|
+
# public again (`public_class_method :new`, a template-method `def self.x`);
|
|
3008
|
+
# - a call inside a `def` ({#load_time_call?}): code that runs while the files load (a class body's
|
|
3009
|
+
# `ALL = [K.new]` before its `private_class_method :new`) may run before the call is private;
|
|
3010
|
+
# - no class on the chain may have had every singleton name made public (`K.public_class_method(*names)`,
|
|
3011
|
+
# `K.singleton_class.class_eval { … }`; {#singleton_wildcard_contested?}).
|
|
3012
|
+
def singleton_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
3013
|
+
class_name = receiver_type.class_name
|
|
3014
|
+
return nil unless call_node.receiver.is_a?(Prism::ConstantReadNode) ||
|
|
3015
|
+
call_node.receiver.is_a?(Prism::ConstantPathNode)
|
|
3016
|
+
return nil unless private_singleton_candidate?(scope, call_node.name)
|
|
3017
|
+
return nil if load_time_call?(scope, call_node)
|
|
3018
|
+
return nil unless reached_definer_visibility(scope, class_name, call_node.name, :singleton) == :private
|
|
3019
|
+
return nil if missing_hook_on_chain?(scope, class_name, :singleton)
|
|
3020
|
+
return nil if singleton_wildcard_contested?(scope, class_name)
|
|
3021
|
+
return nil if refined_method_in_effect?(class_name, call_node, scope, :singleton, lexical_sites)
|
|
3022
|
+
|
|
3023
|
+
build_visibility_mismatch_diagnostic(path, call_node, "singleton(#{class_name})")
|
|
3024
|
+
end
|
|
3025
|
+
|
|
3026
|
+
PRIVATE_NAMES_MEMO = :rigor_check_rules_private_singleton_names
|
|
3027
|
+
private_constant :PRIVATE_NAMES_MEMO
|
|
3028
|
+
|
|
3029
|
+
# Issue #1984 — the table-only gate the singleton side asks before it reads a chain, as the instance side asks
|
|
3030
|
+
# its own table first: whether any class records `name` private, on its singleton side or on its instance
|
|
3031
|
+
# side (an extended module's private method is private on the extender's singleton). Without it every
|
|
3032
|
+
# `Widget.new` would read, and file the dependency edges of, the whole singleton chain. The set is memoised per
|
|
3033
|
+
# discovery index.
|
|
3034
|
+
def private_singleton_candidate?(scope, name)
|
|
3035
|
+
discovery = scope.discovery
|
|
3036
|
+
slot = Thread.current[PRIVATE_NAMES_MEMO]
|
|
3037
|
+
unless slot && slot[0].equal?(discovery)
|
|
3038
|
+
slot = [discovery, private_recorded_names(discovery.discovered_method_visibilities)]
|
|
3039
|
+
Thread.current[PRIVATE_NAMES_MEMO] = slot
|
|
3040
|
+
end
|
|
3041
|
+
slot[1].include?(name.to_sym)
|
|
3042
|
+
end
|
|
3043
|
+
|
|
3044
|
+
def private_recorded_names(tables)
|
|
3045
|
+
tables.each_value.with_object(Set.new) do |table, names|
|
|
3046
|
+
table.each { |slot, visibility| names << (slot.is_a?(Array) ? slot.last : slot) if visibility == :private }
|
|
3047
|
+
end
|
|
3048
|
+
end
|
|
3049
|
+
|
|
3050
|
+
# Issue #1984 — whether the call is outside every `def` the file's deferred ranges name. A `def` is named
|
|
3051
|
+
# wherever it is written in a class body, at the top level or in a `class_eval` block, so a call in it is not
|
|
3052
|
+
# load time and may be reported. The top level, class bodies, and blocks and lambdas that are not such a `def`
|
|
3053
|
+
# (a `map { … }` or `define_method` block, a `-> { }`) are load time and decline: such code may run while the
|
|
3054
|
+
# files load, before a later `private_class_method` or in a load order where the private declaration has not
|
|
3055
|
+
# run yet.
|
|
3056
|
+
def load_time_call?(scope, call_node)
|
|
3057
|
+
rows = scope.discovered_deferred_ranges[scope.source_path]
|
|
3058
|
+
return true if rows.nil?
|
|
3059
|
+
|
|
3060
|
+
offset = call_node.location.start_offset
|
|
3061
|
+
rows.none? { |(start, finish, name, _kind, _owner)| !name.nil? && start <= offset && offset < finish }
|
|
3062
|
+
end
|
|
3063
|
+
|
|
3064
|
+
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
3065
|
+
private_constant :SINGLETON_WILDCARD_SLOT
|
|
3066
|
+
|
|
3067
|
+
# Issue #1984 — whether a project class on `class_name`'s singleton chain has its wildcard singleton slot
|
|
3068
|
+
# contested: a call the walk saw may have made any of its singleton methods public.
|
|
3069
|
+
def singleton_wildcard_contested?(scope, class_name)
|
|
3070
|
+
discovery = scope.discovery
|
|
3071
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3072
|
+
chain.entries.any? do |entry|
|
|
3073
|
+
!entry.external? && discovery.contested?(:discovered_method_visibilities,
|
|
3074
|
+
[entry.name, SINGLETON_WILDCARD_SLOT])
|
|
3075
|
+
end
|
|
3076
|
+
end
|
|
3077
|
+
|
|
3078
|
+
VISIBILITY_MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
|
|
3079
|
+
private_constant :VISIBILITY_MISSING_HOOKS
|
|
3080
|
+
|
|
3081
|
+
# Issue #1984 — whether a project entry on `class_name`'s chain for `side` records a `method_missing` or
|
|
3082
|
+
# `respond_to_missing?` on the side that entry stands for (a singleton entry's singleton methods, a mixin's
|
|
3083
|
+
# instance methods), or the chain cannot be read whole. Ruby sends a private call to `method_missing`, so
|
|
3084
|
+
# such a hook may answer it.
|
|
3085
|
+
def missing_hook_on_chain?(scope, class_name, side)
|
|
3086
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, :methods)
|
|
3087
|
+
return true if chain.truncated?
|
|
3088
|
+
|
|
3089
|
+
chain.entries.any? do |entry|
|
|
3090
|
+
next false if entry.external?
|
|
3091
|
+
|
|
3092
|
+
kind = entry.side == :singleton ? :singleton : :instance
|
|
3093
|
+
VISIBILITY_MISSING_HOOKS.any? { |hook| records_hook?(scope, entry.name, hook, kind) }
|
|
3094
|
+
end
|
|
3095
|
+
end
|
|
3096
|
+
|
|
3097
|
+
def records_hook?(scope, owner, hook, kind)
|
|
3098
|
+
return true if scope.discovered_method?(owner, hook, kind)
|
|
3099
|
+
|
|
3100
|
+
!(kind == :singleton ? scope.singleton_def_for(owner, hook) : scope.user_def_for(owner, hook)).nil?
|
|
2881
3101
|
end
|
|
2882
3102
|
|
|
2883
3103
|
# Issue #1568, ADR-119 C1c — the recorded visibility of the definition a call on `class_name` reaches: the
|
|
@@ -2889,8 +3109,8 @@ module Rigor
|
|
|
2889
3109
|
# producers contest is not alone (`DefinerResolution` asks the next candidate too), so it is `:undecided`
|
|
2890
3110
|
# there without a read of the table here. nil is nothing recording the name at all, and the class's own
|
|
2891
3111
|
# visibility stands. An ancestor the project does not declare is passed over, as the method lookup passes it.
|
|
2892
|
-
def reached_definer_visibility(scope, class_name, method_name)
|
|
2893
|
-
case Inference::DefinerResolution.resolve(scope, class_name, method_name,
|
|
3112
|
+
def reached_definer_visibility(scope, class_name, method_name, side = :instance)
|
|
3113
|
+
case Inference::DefinerResolution.resolve(scope, class_name, method_name, side, question: :visibility)
|
|
2894
3114
|
in Inference::DefinerResolution::Known(answer:)
|
|
2895
3115
|
answer
|
|
2896
3116
|
in Inference::DefinerResolution::UNKNOWN
|
|
@@ -2907,14 +3127,14 @@ module Rigor
|
|
|
2907
3127
|
true
|
|
2908
3128
|
end
|
|
2909
3129
|
|
|
2910
|
-
def build_visibility_mismatch_diagnostic(path, call_node,
|
|
3130
|
+
def build_visibility_mismatch_diagnostic(path, call_node, receiver_name)
|
|
2911
3131
|
Diagnostic.from_message_loc(
|
|
2912
3132
|
call_node,
|
|
2913
3133
|
rule: RULE_VISIBILITY_MISMATCH,
|
|
2914
3134
|
path: path,
|
|
2915
|
-
message: "private method `#{call_node.name}' called on #{
|
|
3135
|
+
message: "private method `#{call_node.name}' called on #{receiver_name} receiver",
|
|
2916
3136
|
severity: :error,
|
|
2917
|
-
receiver_type:
|
|
3137
|
+
receiver_type: receiver_name,
|
|
2918
3138
|
method_name: call_node.name.to_s
|
|
2919
3139
|
)
|
|
2920
3140
|
end
|
|
@@ -3991,6 +4211,9 @@ module Rigor
|
|
|
3991
4211
|
# prove a reduction, stay silent.
|
|
3992
4212
|
return nil if parent_visibility.nil?
|
|
3993
4213
|
return nil unless visibility_reduced?(parent_visibility, override_visibility)
|
|
4214
|
+
# Issue #1569 — a module function's instance copy is private because `module_function` made it so; the
|
|
4215
|
+
# author's intent is the public singleton copy, so that reduction is not reported.
|
|
4216
|
+
return nil if scope.module_function_def?(class_name, def_node)
|
|
3994
4217
|
|
|
3995
4218
|
build_override_visibility_diagnostic(
|
|
3996
4219
|
path, def_node, parent_class, parent_visibility, override_visibility
|
|
@@ -6,6 +6,7 @@ require_relative "return_type_heuristic"
|
|
|
6
6
|
require_relative "../../source/constant_path"
|
|
7
7
|
require_relative "../../source/node_children"
|
|
8
8
|
require_relative "../../inference/scope_indexer"
|
|
9
|
+
require_relative "../../rooted_glob"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module Analysis
|
|
@@ -127,7 +128,7 @@ module Rigor
|
|
|
127
128
|
def walk_root(root_dir, harvest, budget) # rubocop:disable Naming/PredicateMethod
|
|
128
129
|
return false unless File.directory?(root_dir)
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
RootedGlob.glob(root_dir, "**/*.rb").each do |path|
|
|
131
132
|
harvest_file(path, harvest, budget)
|
|
132
133
|
return true if harvest.catalog.size >= budget
|
|
133
134
|
end
|
|
@@ -120,6 +120,18 @@ module Rigor
|
|
|
120
120
|
appeared.freeze
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# Issue #1459 — the method names some file in `paths` started or stopped defining in this edit, on any class or
|
|
124
|
+
# side. A consumer whose answer asked whether the program defines a name anywhere
|
|
125
|
+
# (`Inference::BlockCallTiming.project_defines_anywhere?`) recorded a `defines:<name>` edge whichever way it
|
|
126
|
+
# answered. `paths` MUST include removed files, whose every name vanished. `before` / `after` map a path to the
|
|
127
|
+
# Set of names it defines (`Inference::ScopeIndexer.defined_method_names`); a path absent from either side
|
|
128
|
+
# defines nothing there.
|
|
129
|
+
def changed_defined_names(paths, before, after)
|
|
130
|
+
moved = Set.new
|
|
131
|
+
paths.each { |path| moved.merge((before[path] || Set.new) ^ (after[path] || Set.new)) }
|
|
132
|
+
moved.freeze
|
|
133
|
+
end
|
|
134
|
+
|
|
123
135
|
# Issue #1120 — the method names whose refinements moved in this edit. A consumer whose answer consulted the
|
|
124
136
|
# refinement table recorded a `refinement:<method>` edge whichever way it answered, because the answer is a
|
|
125
137
|
# function of the whole project's refinements of that name. `paths` MUST include removed files, whose whole
|
|
@@ -182,6 +194,55 @@ module Rigor
|
|
|
182
194
|
moved.freeze
|
|
183
195
|
end
|
|
184
196
|
|
|
197
|
+
# Issue #1833 — the `[class name, mark]` pairs that appeared in or vanished from the class marks of the files an
|
|
198
|
+
# edit touched: a Symbol key of a `discovered_parameter_envelopes` bucket (the class-wide marks of
|
|
199
|
+
# `Scope::DiscoveryIndex`) or a `[ENVELOPE_REMOVED_KIND, name]` entry. A file may mark a class it does not declare
|
|
200
|
+
# (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a method body), so neither the class-declaration
|
|
201
|
+
# diff nor a reader's edge to the declaring files sees such a mark appear or vanish.
|
|
202
|
+
#
|
|
203
|
+
# `before` and `after` are lists of envelope tables (`{class name => bucket}`): the touched files' seed-bundle
|
|
204
|
+
# tables before the edit, and the merged table of the re-parsed files after it. The diff is of their unions, which
|
|
205
|
+
# moves whenever the project's union of marks moves (the untouched files contribute the same marks to both).
|
|
206
|
+
# Returns a frozen Set of frozen `[class name, mark]` pairs.
|
|
207
|
+
def changed_class_marks(before, after)
|
|
208
|
+
(class_marks(before) ^ class_marks(after)).freeze
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def class_marks(tables)
|
|
212
|
+
marks = Set.new
|
|
213
|
+
tables.each do |table|
|
|
214
|
+
table&.each do |class_name, bucket|
|
|
215
|
+
bucket.each_key { |key| marks << [class_name, key].freeze if class_mark?(key) }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
marks
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def class_mark?(key)
|
|
222
|
+
key.is_a?(Symbol) || (key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Issue #1661 — the names of the classes and modules whose unpositioned mixin edges
|
|
226
|
+
# (`Scope::DiscoveryIndex#unpositioned_mixins`, `"*"` included) appeared or vanished in the files an edit
|
|
227
|
+
# touched. A file can taint a module it does not otherwise change the chain of (`module Comparable;
|
|
228
|
+
# include(*MODS); end`, `Comparable.include(x) if c`), and an external chain entry has no declaring file
|
|
229
|
+
# for a reader's edge to name. `before` and `after` are lists of `{class => {side => [names]}}` tables,
|
|
230
|
+
# diffed as unions like {changed_class_marks}. Returns a frozen Set of class names.
|
|
231
|
+
def changed_unpositioned_owners(before, after)
|
|
232
|
+
moved = unpositioned_edges(before) ^ unpositioned_edges(after)
|
|
233
|
+
moved.to_set { |(owner, _side, _name)| owner }.freeze
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def unpositioned_edges(tables)
|
|
237
|
+
edges = Set.new
|
|
238
|
+
tables.each do |table|
|
|
239
|
+
table&.each do |owner, sides|
|
|
240
|
+
sides.each { |side, names| names.each { |name| edges << [owner, side, name].freeze } }
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
edges
|
|
244
|
+
end
|
|
245
|
+
|
|
185
246
|
# Issue #644 — the qualified constant names whose PUBLICATION could have moved in this edit: the names
|
|
186
247
|
# whose per-file census descriptor differs between the before- and after-state of any file in `paths`.
|
|
187
248
|
# The value-constant twin of {appeared_classes}, and deliberately NOT its analogue — a class either
|