rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../configuration/severity_profile"
|
|
6
|
+
require_relative "../../reflection"
|
|
7
|
+
require_relative "../../source/node_walker"
|
|
8
|
+
require_relative "../../scope/resolution_chain"
|
|
9
|
+
require_relative "../../type"
|
|
10
|
+
require_relative "../diagnostic"
|
|
11
|
+
require_relative "rule_ids"
|
|
12
|
+
|
|
13
|
+
module Rigor
|
|
14
|
+
module Analysis
|
|
15
|
+
module CheckRules
|
|
16
|
+
# ADR-128 WD6 / ADR-130 WD3 — `plugin.api-surface`. A call in plugin source on a value typed by a
|
|
17
|
+
# `Rigor::Plugin` interface (`Dynamic[Rigor::Plugin::_Scope]`, ...) to a member that interface does not
|
|
18
|
+
# declare. The method exists on the backing engine class, so the call works today; it is outside what Rigor
|
|
19
|
+
# promises to keep, so `call.undefined-method` would be wrong and this rule says that instead.
|
|
20
|
+
#
|
|
21
|
+
# Authored `:warning`, resolved `:off` by every profile and promoted only by the `plugin-api-surface`
|
|
22
|
+
# bleeding-edge feature (ADR-50 Decision 3, WD2), as `static.value-use.void` is.
|
|
23
|
+
#
|
|
24
|
+
# It fires only where all of these hold:
|
|
25
|
+
#
|
|
26
|
+
# - the call has an explicit receiver, typed `Dynamic[<Rigor::Plugin interface>]`, or that plus `nil` (the
|
|
27
|
+
# facet survives only where ADR-130 WD1 keeps it; a value that lost it at a join reads `Dynamic[top]`);
|
|
28
|
+
# - the interface declares no member of the name, and `Object` (with `Kernel` and `BasicObject`) declares
|
|
29
|
+
# none either: `nil?`, `respond_to?` and `is_a?` are every object's surface, not the engine's;
|
|
30
|
+
# - the call sits lexically inside a class whose discovered superclass chain reaches `Rigor::Plugin::Base`.
|
|
31
|
+
# `Base` itself is not such a class, so its own hooks, which hand interface values to engine helpers
|
|
32
|
+
# (`environment.class_ordering`), are not judged: they are engine code.
|
|
33
|
+
#
|
|
34
|
+
# The plugin-source test reads the class declarations, not a path: a third-party plugin gem keeps its plugin
|
|
35
|
+
# under `lib/` as Rigor's own engine does.
|
|
36
|
+
#
|
|
37
|
+
# The rule runs only when its resolved severity is not `:off` ({.active?}): the runner then adds its
|
|
38
|
+
# {Collector} to the shared {RuleWalk}. A default run builds no collector, so it types no extra receiver and
|
|
39
|
+
# files no ADR-46 dependency for the rule.
|
|
40
|
+
module PluginApiSurface
|
|
41
|
+
PLUGIN_BASE = "Rigor::Plugin::Base"
|
|
42
|
+
|
|
43
|
+
# One call the rule reports: the call and the first interface its receiver carries.
|
|
44
|
+
Result = Data.define(:call_node, :interface)
|
|
45
|
+
|
|
46
|
+
# The {RuleWalk} collector: one {Result} per reported call, in walk order.
|
|
47
|
+
class Collector
|
|
48
|
+
NODE_CLASSES = [Prism::CallNode].freeze
|
|
49
|
+
|
|
50
|
+
def initialize(scope_index)
|
|
51
|
+
@scope_index = scope_index
|
|
52
|
+
@results = []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The legacy single-collector walk, the oracle the ADR-53 equivalence harness compares {RuleWalk} with.
|
|
56
|
+
def collect(root)
|
|
57
|
+
Source::NodeWalker.each(root) { |node| visit(node) if node.is_a?(Prism::CallNode) }
|
|
58
|
+
@results.freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def visit(node, _context = nil)
|
|
62
|
+
interface = PluginApiSurface.outside_interface(node, @scope_index[node])
|
|
63
|
+
@results << Result.new(call_node: node, interface: interface) if interface
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def results
|
|
67
|
+
@results.freeze
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
module_function
|
|
72
|
+
|
|
73
|
+
# Whether the rule can report in a run under `configuration`: it is not in `disable:`, and its resolved
|
|
74
|
+
# severity is not `:off`. Default profiles resolve it `:off`; the `plugin-api-surface` bleeding-edge feature
|
|
75
|
+
# or an explicit `severity_overrides:` entry for the id turns it on.
|
|
76
|
+
def active?(configuration)
|
|
77
|
+
disabled = configuration.disabled_rules
|
|
78
|
+
return false if disabled.include?(RULE_PLUGIN_API_SURFACE) || disabled.include?("all")
|
|
79
|
+
|
|
80
|
+
Configuration::SeverityProfile.resolve(
|
|
81
|
+
rule: RULE_PLUGIN_API_SURFACE, authored_severity: :warning,
|
|
82
|
+
profile: configuration.severity_profile, overrides: configuration.severity_overrides,
|
|
83
|
+
bleeding_edge_overrides: configuration.bleeding_edge_severity_overrides
|
|
84
|
+
) != :off
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The interface the call's receiver carries when the call is outside it, or nil.
|
|
88
|
+
def outside_interface(call_node, scope)
|
|
89
|
+
return nil if call_node.receiver.nil? || scope.nil?
|
|
90
|
+
|
|
91
|
+
interfaces = receiver_interfaces(scope.type_of(call_node.receiver))
|
|
92
|
+
return nil if interfaces.nil?
|
|
93
|
+
|
|
94
|
+
environment = scope.environment
|
|
95
|
+
method_name = call_node.name
|
|
96
|
+
return nil if interfaces.any? { |interface| declares?(environment, interface, method_name) }
|
|
97
|
+
return nil if object_member?(environment, method_name)
|
|
98
|
+
return nil unless plugin_source?(scope)
|
|
99
|
+
|
|
100
|
+
interfaces.first
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def diagnostics(path, results)
|
|
104
|
+
results.map { |result| build(path, result.call_node, result.interface, result.call_node.name) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The interface facets the receiver carries, or nil when it is anything but `Dynamic[Interface]` arms and
|
|
108
|
+
# `nil`. A union whose non-nil arms include another type has a member the call may resolve on.
|
|
109
|
+
def receiver_interfaces(type)
|
|
110
|
+
case type
|
|
111
|
+
when Type::Dynamic
|
|
112
|
+
facet = type.static_facet
|
|
113
|
+
[facet] if facet.is_a?(Type::Interface)
|
|
114
|
+
when Type::Union
|
|
115
|
+
union_interfaces(type)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def union_interfaces(union)
|
|
120
|
+
interfaces = []
|
|
121
|
+
union.members.each do |member|
|
|
122
|
+
next if nil_type?(member)
|
|
123
|
+
return nil unless member.is_a?(Type::Dynamic) && member.static_facet.is_a?(Type::Interface)
|
|
124
|
+
|
|
125
|
+
interfaces << member.static_facet
|
|
126
|
+
end
|
|
127
|
+
interfaces.empty? ? nil : interfaces
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def nil_type?(type)
|
|
131
|
+
(type.is_a?(Type::Constant) && type.value.nil?) ||
|
|
132
|
+
(type.is_a?(Type::Nominal) && type.class_name == "NilClass")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# An interface that does not resolve declares nothing the rule can read, so it is treated as declaring
|
|
136
|
+
# every member. A guard: the carrier exists only for an interface the loader translated, so no fixture
|
|
137
|
+
# reaches it.
|
|
138
|
+
def declares?(environment, interface, method_name)
|
|
139
|
+
definition = environment.rbs_loader.interface_definition(interface.name)
|
|
140
|
+
return true if definition.nil?
|
|
141
|
+
|
|
142
|
+
definition.methods.key?(method_name.to_sym)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def object_member?(environment, method_name)
|
|
146
|
+
!Rigor::Reflection.instance_method_definition("Object", method_name, environment: environment).nil?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Whether the call is plugin source: lexically inside a class that inherits `Rigor::Plugin::Base`. A
|
|
150
|
+
# `Rigor::Plugin::Base` body is not: its chain holds `Base` only as itself. An unrecorded nesting (a scope
|
|
151
|
+
# built outside a declaration walk) is not either.
|
|
152
|
+
def plugin_source?(scope)
|
|
153
|
+
nesting = scope.lexical_nesting
|
|
154
|
+
return false if nesting.nil? || nesting.empty?
|
|
155
|
+
|
|
156
|
+
nesting.any? { |name| inherits_plugin_base?(scope, name) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Whether `class_name`'s instance chain ({Scope::ResolutionChain}, the one ancestry reader) holds
|
|
160
|
+
# `Rigor::Plugin::Base` below the class itself. Reading it files the ADR-46 class dependencies, so
|
|
161
|
+
# an incremental recheck re-judges the call when a declaration on the chain changes in another file. A
|
|
162
|
+
# truncated chain reads as no plugin: the rule withholds where it cannot see the whole chain.
|
|
163
|
+
def inherits_plugin_base?(scope, class_name)
|
|
164
|
+
root = class_name.to_s
|
|
165
|
+
chain = Scope::ResolutionChain.for(scope, root, :instance, :methods)
|
|
166
|
+
chain.record(scope)
|
|
167
|
+
return false if chain.truncated?
|
|
168
|
+
|
|
169
|
+
chain.entries.any? { |entry| entry.name != root && plugin_base_entry?(entry) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# `Rigor::Plugin::Base` is external where only its RBS and the gem declare it, and a project entry where the
|
|
173
|
+
# analysed tree holds its source (Rigor's own `lib`, or a reopen).
|
|
174
|
+
def plugin_base_entry?(entry)
|
|
175
|
+
entry.external? ? entry.candidates.include?(PLUGIN_BASE) : entry.name == PLUGIN_BASE
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def build(path, call_node, interface, method_name)
|
|
179
|
+
Diagnostic.from_message_loc(
|
|
180
|
+
call_node,
|
|
181
|
+
rule: RULE_PLUGIN_API_SURFACE,
|
|
182
|
+
path: path,
|
|
183
|
+
message: "`#{method_name}' is not part of the plugin API: `#{interface.describe}' does not declare it. " \
|
|
184
|
+
"The engine object answers it today, but Rigor does not promise to keep it; call a member the " \
|
|
185
|
+
"interface declares (sig/rigor/plugin/interfaces.rbs) or a public namespace " \
|
|
186
|
+
"(docs/internal-spec/public-api.md)",
|
|
187
|
+
severity: :warning,
|
|
188
|
+
receiver_type: nil,
|
|
189
|
+
method_name: method_name
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -115,15 +115,19 @@ module Rigor
|
|
|
115
115
|
# exemption exactly rather than on the last segment. A path that renders no qualified name falls back
|
|
116
116
|
# to its own last segment, which is all there is to ask about.
|
|
117
117
|
def published_path?(node, scope)
|
|
118
|
-
rendered =
|
|
118
|
+
rendered = constant_spelling(node)
|
|
119
119
|
!rendered.nil? && scope.published_constant?(rendered)
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# The one hop. Resolves the callee through the ordinary `Scope` accessors (so the ADR-46 recorder sees
|
|
123
123
|
# the read and the caller gains an edge to the callee — the conservative direction) and scans its
|
|
124
124
|
# body. Fails soft to false: a resolution this cannot make is not a reason to withhold.
|
|
125
|
+
#
|
|
126
|
+
# Issue #3199 — the empty-table shortcut is taken only when no ADR-46 recorder is watching: the scan's
|
|
127
|
+
# `Scope#published_constant?` questions are what record the `constant:` edges, and a reader that skipped
|
|
128
|
+
# them would not be re-checked when another file publishes a constant the callee's body names.
|
|
125
129
|
def callee_reads_published_constant?(node, scope)
|
|
126
|
-
return false if scope.published_constant_names.empty?
|
|
130
|
+
return false if scope.published_constant_names.empty? && !Analysis::DependencyRecorder.recording?
|
|
127
131
|
|
|
128
132
|
def_node = resolve_callee(node, scope)
|
|
129
133
|
return false if def_node.nil?
|
|
@@ -172,10 +176,30 @@ module Rigor
|
|
|
172
176
|
end
|
|
173
177
|
end
|
|
174
178
|
|
|
175
|
-
# An explicit worklist rather than recursion, so the node budget is one loop-carried local instead of
|
|
176
|
-
# a shared mutable cell (the engine value-pins a one-element Array counter and then self-flags the
|
|
177
|
-
# guard's own `negative?` as an always-falsey condition — this rule's own medicine).
|
|
178
179
|
def body_reads_published_constant?(root, scope)
|
|
180
|
+
scan_body(root) { |node| return true if published_constant_node?(node, scope) }
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Issue #3186 — the constant spellings the one hop's body scan would ask `Scope#published_constant?` about,
|
|
185
|
+
# sorted and de-duplicated: every node {#body_reads_published_constant?} visits within its budget, in its
|
|
186
|
+
# order. The hop's answer for any reader is a function of this list and of that reader's publication
|
|
187
|
+
# tables, so the ADR-89 WD2 gate stores it beside a def's return summary and compares it on an edit: a
|
|
188
|
+
# reader the gate skips would otherwise keep a withholding (or a firing) the edited body no longer earns.
|
|
189
|
+
def body_constant_spellings(root)
|
|
190
|
+
spellings = []
|
|
191
|
+
scan_body(root) do |node|
|
|
192
|
+
spelling = constant_spelling(node)
|
|
193
|
+
spellings << spelling unless spelling.nil?
|
|
194
|
+
end
|
|
195
|
+
spellings.uniq.sort
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Yields each node of `root` the hop's scan visits, stopping once {BODY_SCAN_BUDGET} is spent. An explicit
|
|
199
|
+
# worklist rather than recursion, so the node budget is one loop-carried local instead of a shared mutable
|
|
200
|
+
# cell (the engine value-pins a one-element Array counter and then self-flags the guard's own `negative?`
|
|
201
|
+
# as an always-falsey condition — this rule's own medicine).
|
|
202
|
+
def scan_body(root)
|
|
179
203
|
stack = [root]
|
|
180
204
|
budget = BODY_SCAN_BUDGET
|
|
181
205
|
until stack.empty?
|
|
@@ -183,19 +207,24 @@ module Rigor
|
|
|
183
207
|
next unless node.is_a?(Prism::Node)
|
|
184
208
|
|
|
185
209
|
budget -= 1
|
|
186
|
-
return
|
|
187
|
-
return true if published_constant_node?(node, scope)
|
|
210
|
+
return if budget.negative?
|
|
188
211
|
|
|
212
|
+
yield node
|
|
189
213
|
node.rigor_each_child { |child| stack << child }
|
|
190
214
|
end
|
|
191
|
-
false
|
|
192
215
|
end
|
|
193
216
|
|
|
194
217
|
def published_constant_node?(node, scope)
|
|
218
|
+
spelling = constant_spelling(node)
|
|
219
|
+
!spelling.nil? && scope.published_constant?(spelling)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The name a constant reference asks the publication tables about ({#published_path?}'s rendering for a
|
|
223
|
+
# path), or nil for any other node and for a path that renders no name.
|
|
224
|
+
def constant_spelling(node)
|
|
195
225
|
case node
|
|
196
|
-
when Prism::ConstantReadNode then
|
|
197
|
-
when Prism::ConstantPathNode then
|
|
198
|
-
else false
|
|
226
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
227
|
+
when Prism::ConstantPathNode then Source::ConstantPath.qualified_name_or_nil(node) || node.name&.to_s
|
|
199
228
|
end
|
|
200
229
|
end
|
|
201
230
|
|
|
@@ -36,10 +36,36 @@ module Rigor
|
|
|
36
36
|
MISSING_HOOK = :method_missing
|
|
37
37
|
# The roots whose project mixins `Inference::ObjectMixins` answers for.
|
|
38
38
|
OBJECT_ROOTS = %w[Object Kernel BasicObject].to_set.freeze
|
|
39
|
-
|
|
39
|
+
# Issue #2792 — the owners of a `method_missing` every object (and every class object) reaches. Core's
|
|
40
|
+
# `BasicObject#method_missing` raises `NoMethodError`, so it opens nothing; core declares none on the others,
|
|
41
|
+
# which are here so that a project or gem RBS reopening `Object`, `Kernel`, `Module` or `Class` with one cannot
|
|
42
|
+
# silence the rule for every receiver.
|
|
43
|
+
UNIVERSAL_HOOK_OWNERS = {
|
|
44
|
+
instance: OBJECT_ROOTS,
|
|
45
|
+
singleton: (OBJECT_ROOTS | %w[Module Class]).freeze
|
|
46
|
+
}.freeze
|
|
47
|
+
private_constant :MISSING_HOOK, :OBJECT_ROOTS, :UNIVERSAL_HOOK_OWNERS
|
|
40
48
|
|
|
41
49
|
module_function
|
|
42
50
|
|
|
51
|
+
# Issue #2792 — whether RBS gives `class_name`'s `side` a `method_missing` from somewhere other than the
|
|
52
|
+
# universal roots: the class itself, an RBS ancestor or an included module (`Delegator` for a
|
|
53
|
+
# `SimpleDelegator`, `class W < SimpleDelegator`, `OpenStruct`). Ruby sends any name the receiver lacks there,
|
|
54
|
+
# private or public, so no name is provably missing. Only the absence verdict reads it: the call's type stays
|
|
55
|
+
# what an unresolved call answers.
|
|
56
|
+
def rbs_missing_hook?(scope, class_name, side)
|
|
57
|
+
definition =
|
|
58
|
+
if side == :singleton
|
|
59
|
+
Rigor::Reflection.singleton_method_definition(class_name, MISSING_HOOK, scope: scope)
|
|
60
|
+
else
|
|
61
|
+
Rigor::Reflection.instance_method_definition(class_name, MISSING_HOOK, scope: scope)
|
|
62
|
+
end
|
|
63
|
+
owner = definition&.defined_in
|
|
64
|
+
return false if owner.nil?
|
|
65
|
+
|
|
66
|
+
!UNIVERSAL_HOOK_OWNERS.fetch(side).include?(owner.to_s.delete_prefix("::"))
|
|
67
|
+
end
|
|
68
|
+
|
|
43
69
|
# Whether the project may answer `method_name` on an instance of `class_name` through one of its RBS
|
|
44
70
|
# ancestors.
|
|
45
71
|
def ancestor_may_answer?(scope, class_name, method_name)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../inference/scope_indexer"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Analysis
|
|
9
|
+
module CheckRules
|
|
10
|
+
# Issue #3155 — the singleton defs of one file that a later def of the same name certainly replaces, which
|
|
11
|
+
# `def.return-type-mismatch` does not check against the class's signature: Ruby keeps the later body.
|
|
12
|
+
#
|
|
13
|
+
# Both defs must be certain singleton defs of the same class or module body: a direct statement of that body
|
|
14
|
+
# (`def self.x`, or `def K.x` where `K` names the class), or a direct statement of a `class << self` there
|
|
15
|
+
# (`def x`, or `def K.x`). A def in a block (`Class.new do … end`, `Struct.new(…) do … end`), under a condition,
|
|
16
|
+
# loop or `begin`/`rescue`, or in a method body may not run, or runs on another object, so it replaces nothing,
|
|
17
|
+
# and a def written there is never read as replaced either. A reopening in another file is not read.
|
|
18
|
+
#
|
|
19
|
+
# The walk groups defs by the class or module body node only; whether a constant receiver names the class is
|
|
20
|
+
# read from the def's own Scope (`self_type`, the indexer's declaration context), so it derives none itself.
|
|
21
|
+
class ReplacedSingletonDefs
|
|
22
|
+
def initialize(root, scope_index)
|
|
23
|
+
@root = root
|
|
24
|
+
@scope_index = scope_index
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def replaced?(def_node) = offsets.include?(def_node.location.start_offset)
|
|
28
|
+
|
|
29
|
+
# `def (self).x` and `def (K).x` name the receiver inside parentheses.
|
|
30
|
+
def self.unparenthesized(node)
|
|
31
|
+
while node.is_a?(Prism::ParenthesesNode)
|
|
32
|
+
body = node.body
|
|
33
|
+
node = body.is_a?(Prism::StatementsNode) && body.body.size == 1 ? body.body.first : body
|
|
34
|
+
end
|
|
35
|
+
node
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def offsets
|
|
41
|
+
@offsets ||= collect
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def collect
|
|
45
|
+
replaced = Set.new
|
|
46
|
+
each_class_body(@root) do |body|
|
|
47
|
+
last = {}
|
|
48
|
+
certain_singleton_defs(body).each do |def_node|
|
|
49
|
+
replaced << last[def_node.name].location.start_offset if last.key?(def_node.name)
|
|
50
|
+
last[def_node.name] = def_node
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
replaced
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def each_class_body(node, &)
|
|
57
|
+
return if node.nil?
|
|
58
|
+
|
|
59
|
+
yield node.body if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
60
|
+
node.rigor_each_child { |child| each_class_body(child, &) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def certain_singleton_defs(body)
|
|
64
|
+
statements(body).flat_map do |stmt|
|
|
65
|
+
case stmt
|
|
66
|
+
when Prism::DefNode
|
|
67
|
+
lexical_self?(stmt.receiver, stmt, self_allowed: true) ? [stmt] : []
|
|
68
|
+
when Prism::SingletonClassNode
|
|
69
|
+
next [] unless lexical_self?(stmt.expression, stmt, self_allowed: true)
|
|
70
|
+
|
|
71
|
+
statements(stmt.body).select do |inner|
|
|
72
|
+
inner.is_a?(Prism::DefNode) &&
|
|
73
|
+
(inner.receiver.nil? || lexical_self?(inner.receiver, inner, self_allowed: false))
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
[]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `at` is the node whose Scope names the class: its `self_type` is the class or its singleton.
|
|
82
|
+
def lexical_self?(node, at, self_allowed:)
|
|
83
|
+
node = self.class.unparenthesized(node)
|
|
84
|
+
return self_allowed if node.is_a?(Prism::SelfNode)
|
|
85
|
+
|
|
86
|
+
self_type = @scope_index[at]&.self_type
|
|
87
|
+
return false unless self_type.respond_to?(:class_name)
|
|
88
|
+
|
|
89
|
+
Inference::ScopeIndexer.def_receiver_targets_lexical_self?(node, self_type.class_name.split("::"))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def statements(body) = body.is_a?(Prism::StatementsNode) ? body.body : []
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -81,6 +81,12 @@ module Rigor
|
|
|
81
81
|
# Issue #1692 (the ADR-47 WD5 amendment) — a call to the `ruby2_keywords` family Ruby 4.1 deprecates. Runs only
|
|
82
82
|
# under an explicit `target_ruby` of 4.1 or later; see {RubyDeprecations}.
|
|
83
83
|
RULE_DEPRECATED_RUBY2_KEYWORDS = "call.deprecated-ruby2-keywords"
|
|
84
|
+
# ADR-128 WD6 / ADR-130 slice 2 (#2686) — a call in plugin source on a `Rigor::Plugin` interface value to a
|
|
85
|
+
# member the interface does not declare. Authored `:warning`, resolved `:off` by every profile and promoted
|
|
86
|
+
# only by the `plugin-api-surface` bleeding-edge feature. The one built-in id under `plugin.`, the prefix a
|
|
87
|
+
# plugin's own rows carry as `plugin.<id>.<rule>`: it is in {ALL_RULES}, so `disable:`, markers and
|
|
88
|
+
# `severity_overrides:` resolve it as a catalogue id before any plugin-id reading.
|
|
89
|
+
RULE_PLUGIN_API_SURFACE = "plugin.api-surface"
|
|
84
90
|
|
|
85
91
|
ALL_RULES = [
|
|
86
92
|
RULE_UNDEFINED_METHOD,
|
|
@@ -117,7 +123,8 @@ module Rigor
|
|
|
117
123
|
RULE_EFFECT_LISKOV_WIDENED,
|
|
118
124
|
RULE_EFFECT_UNKNOWN_LABEL,
|
|
119
125
|
RULE_EFFECT_ANNOTATIONS_UNCHECKED,
|
|
120
|
-
RULE_CONTRADICTING_SIGNATURE
|
|
126
|
+
RULE_CONTRADICTING_SIGNATURE,
|
|
127
|
+
RULE_PLUGIN_API_SURFACE
|
|
121
128
|
].freeze
|
|
122
129
|
|
|
123
130
|
# Backward-compat alias table (ADR-8 § "Backward compatibility"). Existing user code with
|
|
@@ -100,7 +100,7 @@ module Rigor
|
|
|
100
100
|
return unless value_node.is_a?(Prism::CallNode)
|
|
101
101
|
|
|
102
102
|
scope = @scope_index[value_node]
|
|
103
|
-
origin = scope&.
|
|
103
|
+
origin = scope&.void_origin(value_node)
|
|
104
104
|
return if origin.nil?
|
|
105
105
|
|
|
106
106
|
@results << Result.new(void_node: value_node, origin: origin)
|