rigortype 0.3.5 → 0.3.7
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/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +298 -38
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../source/constant_path"
|
|
6
|
+
require_relative "../../source/node_children"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Analysis
|
|
10
|
+
module CheckRules
|
|
11
|
+
# Issue #644 — the shared "does this expression's constancy rest on a constant declared in ANOTHER
|
|
12
|
+
# file?" predicate, and the single question the truthiness rule family must ask.
|
|
13
|
+
#
|
|
14
|
+
# The cross-file value-constant table publishes `MODE = :production` project-wide so a reader can
|
|
15
|
+
# dispatch on it, type an argument with it, and see `MODE.upcase` fail. That is the precision the table
|
|
16
|
+
# exists for. What it must NOT do is let a rule *conclude* from the value: `if MODE == :production` in
|
|
17
|
+
# another file folds to `Constant[true]`, and reporting that as `flow.always-truthy-condition` fires on
|
|
18
|
+
# correct code — on the single Ruby idiom whose whole point is a project-wide constant that gets
|
|
19
|
+
# branched on, whose declaration the reader's author never opened. A configuration constant read in ten
|
|
20
|
+
# files would put a warning in all ten.
|
|
21
|
+
#
|
|
22
|
+
# This is [ADR-58](../../../../docs/adr/58-declaration-sourced-ivar-typing.md)'s discipline applied to a
|
|
23
|
+
# different carrier: "a consumer may only ever *withhold* a firing it would otherwise make, so the mark
|
|
24
|
+
# can lose precision but can never manufacture a false positive"
|
|
25
|
+
# (`docs/internal-spec/inference-engine.md` § "Carrier and discipline"). The carrier is deliberately NOT
|
|
26
|
+
# the ADR-58 `Set[[kind, name]]`: that set is flow state, keyed on a *binding* that rebinding drops, and
|
|
27
|
+
# it is already shared by two marks with opposite join policies (ADR-67 WD6b's `:inferred_param` unions
|
|
28
|
+
# where ADR-58's kinds intersect). "Which constants did the project publish, and which does this file
|
|
29
|
+
# declare itself" is ambient, never varies along a flow edge, and so belongs in the
|
|
30
|
+
# `Scope::DiscoveryIndex` — which is exactly ADR-53's membership criterion.
|
|
31
|
+
#
|
|
32
|
+
# `rooted?` is purely syntactic, in the shape of {InferredParamGuard}: it walks a predicate expression
|
|
33
|
+
# down to the roots its constancy could rest on and reports whether any is a published foreign
|
|
34
|
+
# constant. It covers
|
|
35
|
+
#
|
|
36
|
+
# - the bare predicate: `if PAGE_SIZE`, `unless AppConfig::LOG_LEVEL`
|
|
37
|
+
# - a comparison or any method chain on one: `MODE == :production`, `PAGE_SIZE > 100`, `VERSION >= 3`
|
|
38
|
+
# - a constant in ARGUMENT position: `%i[development test].include?(ENV_NAME)` — unlike
|
|
39
|
+
# {InferredParamGuard}, arguments are walked, because a predicate's fold rests on its arguments just
|
|
40
|
+
# as much as on its receiver
|
|
41
|
+
# - `&&` / `||` / parentheses / `rescue` composition, and `!`, which Prism spells as a `CallNode`
|
|
42
|
+
# - ONE interprocedural hop into a project predicate method whose body reads such a constant, whatever
|
|
43
|
+
# spells its owner: `unless production?` (implicit self), `self.prod?`, `AppConfig.production?` (the
|
|
44
|
+
# idiomatic shape — a configuration module exposing predicates), a lexically relative
|
|
45
|
+
# `Inner.production?`, and a call chain `KlassConfig.new.production?`. The owner comes from the
|
|
46
|
+
# receiver's inferred TYPE, so every spelling that names the same module lands on the same table. The
|
|
47
|
+
# hop is bounded (one level, a node budget) and deliberately coarse: ANY published foreign constant
|
|
48
|
+
# anywhere in the callee's body declines, rather than a proof that the return value derives from it.
|
|
49
|
+
# Over-declining withholds a warning; under-declining would emit one. ADR-58's *Non-transitivity*
|
|
50
|
+
# passage is the precedent for stopping at one hop rather than building a provenance channel through
|
|
51
|
+
# the return memo.
|
|
52
|
+
#
|
|
53
|
+
# Deliberately NOT covered, and a separate change: a value copied into a local (`m = MODE; m == :x`),
|
|
54
|
+
# into an ivar (`@mode = MODE` in `initialize`), or through a same-file constant alias
|
|
55
|
+
# (`MODE2 = AppConfig::MODE`). Each needs flow provenance — ADR-58's `r = @x` stamping is the shape —
|
|
56
|
+
# rather than a syntactic root walk, and approximating it here is exactly the ad-hoc widening ADR-58's
|
|
57
|
+
# *Non-transitivity* passage forbids. Two hops (`live? -> prod? -> MODE`) also stay uncovered.
|
|
58
|
+
module PublishedConstantGuard
|
|
59
|
+
# A defensive depth cap against a pathological chain (the walk is otherwise linear in chain length).
|
|
60
|
+
MAX_DEPTH = 64
|
|
61
|
+
|
|
62
|
+
# A node cap on the one-hop callee-body scan, so a predicate calling a very large method cannot make
|
|
63
|
+
# rule collection quadratic. Exhausting it answers false — the firing direction, and the same
|
|
64
|
+
# conservative default the collector's other gates take when they cannot decide.
|
|
65
|
+
BODY_SCAN_BUDGET = 2000
|
|
66
|
+
|
|
67
|
+
module_function
|
|
68
|
+
|
|
69
|
+
# @return [Boolean] true when the rule MUST withhold its firing.
|
|
70
|
+
def rooted?(node, scope, depth = 0)
|
|
71
|
+
return false if node.nil? || depth > MAX_DEPTH || scope.nil?
|
|
72
|
+
|
|
73
|
+
case node
|
|
74
|
+
when Prism::ConstantReadNode then scope.published_constant?(node.name.to_s)
|
|
75
|
+
when Prism::ConstantPathNode then published_path?(node, scope)
|
|
76
|
+
when Prism::CallNode then rooted_call?(node, scope, depth)
|
|
77
|
+
else rooted_through_composition?(node, scope, depth)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `&&` / `||` / `rescue` combine two operands and a parenthesised or multi-statement body yields its
|
|
82
|
+
# last: the produced value is constant only if a rooted operand made it so, so declining on either
|
|
83
|
+
# side is the withholding direction.
|
|
84
|
+
def rooted_through_composition?(node, scope, depth)
|
|
85
|
+
case node
|
|
86
|
+
when Prism::AndNode, Prism::OrNode
|
|
87
|
+
rooted?(node.left, scope, depth + 1) || rooted?(node.right, scope, depth + 1)
|
|
88
|
+
when Prism::RescueModifierNode
|
|
89
|
+
rooted?(node.expression, scope, depth + 1) || rooted?(node.rescue_expression, scope, depth + 1)
|
|
90
|
+
when Prism::ParenthesesNode then rooted?(last_statement(node.body), scope, depth + 1)
|
|
91
|
+
when Prism::StatementsNode then rooted?(node.body.last, scope, depth + 1)
|
|
92
|
+
else false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A call is rooted when its receiver is, when any argument is, or when the project method it names
|
|
97
|
+
# reads such a constant (the one interprocedural hop).
|
|
98
|
+
def rooted_call?(node, scope, depth)
|
|
99
|
+
return true if rooted?(node.receiver, scope, depth + 1)
|
|
100
|
+
return true if Array(node.arguments&.arguments).any? { |arg| rooted?(arg, scope, depth + 1) }
|
|
101
|
+
|
|
102
|
+
callee_reads_published_constant?(node, scope)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The reference AS WRITTEN (`AppConfig::MODE`), so `Scope#published_constant?` can match its local
|
|
106
|
+
# exemption exactly rather than on the last segment. A path that renders no qualified name falls back
|
|
107
|
+
# to its own last segment, which is all there is to ask about.
|
|
108
|
+
def published_path?(node, scope)
|
|
109
|
+
rendered = Source::ConstantPath.qualified_name_or_nil(node) || node.name&.to_s
|
|
110
|
+
!rendered.nil? && scope.published_constant?(rendered)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The one hop. Resolves the callee through the ordinary `Scope` accessors (so the ADR-46 recorder sees
|
|
114
|
+
# the read and the caller gains an edge to the callee — the conservative direction) and scans its
|
|
115
|
+
# body. Fails soft to false: a resolution this cannot make is not a reason to withhold.
|
|
116
|
+
def callee_reads_published_constant?(node, scope)
|
|
117
|
+
return false if scope.published_constant_names.empty?
|
|
118
|
+
|
|
119
|
+
def_node = resolve_callee(node, scope)
|
|
120
|
+
return false if def_node.nil?
|
|
121
|
+
|
|
122
|
+
body_reads_published_constant?(def_node.body, scope)
|
|
123
|
+
rescue StandardError
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# `foo` / `self.foo` resolve against the enclosing self. Every other receiver resolves through its
|
|
128
|
+
# inferred TYPE, never through the name it was spelled with: `Inner.production?` inside `module Outer`
|
|
129
|
+
# is `Outer::Inner`, and keying the table on the bare `Inner` both misses that (a new warning on
|
|
130
|
+
# ordinary code) and, where a top-level `Inner` also exists, reads the WRONG module's methods. That is
|
|
131
|
+
# the defect family #635 fixed in `Narrowing`, so the engine's own resolver answers it rather than a
|
|
132
|
+
# walk reimplemented here. Going through the type also covers a call-chain receiver for free —
|
|
133
|
+
# `KlassConfig.new.production?` types `Nominal[KlassConfig]`, whose instance table is the one to read.
|
|
134
|
+
def resolve_callee(node, scope)
|
|
135
|
+
receiver = node.receiver
|
|
136
|
+
return resolve_self_call(node.name, scope) if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
137
|
+
|
|
138
|
+
resolve_typed_receiver_call(receiver, node.name, scope)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# A `Singleton[X]` receiver names the singleton table (`AppConfig.production?`, a module's own
|
|
142
|
+
# predicates); a `Nominal[X]` receiver names the instance one. Anything else — `Dynamic`, a constant
|
|
143
|
+
# that resolves to no project class — is not a hop this walk can make.
|
|
144
|
+
def resolve_typed_receiver_call(receiver, method_name, scope)
|
|
145
|
+
receiver_type = scope.type_of(receiver)
|
|
146
|
+
case receiver_type
|
|
147
|
+
when Type::Singleton then scope.singleton_def_for(receiver_type.class_name, method_name)
|
|
148
|
+
when Type::Nominal then scope.user_def_for(receiver_type.class_name, method_name)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# A `Singleton[X]` self (a `def self.…` body) names the singleton table; a `Nominal[X]` self names
|
|
153
|
+
# the instance one; no class at all is the top-level pseudo-class.
|
|
154
|
+
def resolve_self_call(method_name, scope)
|
|
155
|
+
self_type = scope.self_type
|
|
156
|
+
owner = self_type.respond_to?(:class_name) ? self_type.class_name : nil
|
|
157
|
+
return scope.top_level_def_for(method_name) if owner.nil?
|
|
158
|
+
|
|
159
|
+
if self_type.is_a?(Type::Singleton)
|
|
160
|
+
scope.singleton_def_for(owner, method_name)
|
|
161
|
+
else
|
|
162
|
+
scope.user_def_for(owner, method_name)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# An explicit worklist rather than recursion, so the node budget is one loop-carried local instead of
|
|
167
|
+
# a shared mutable cell (the engine value-pins a one-element Array counter and then self-flags the
|
|
168
|
+
# guard's own `negative?` as an always-falsey condition — this rule's own medicine).
|
|
169
|
+
def body_reads_published_constant?(root, scope)
|
|
170
|
+
stack = [root]
|
|
171
|
+
budget = BODY_SCAN_BUDGET
|
|
172
|
+
until stack.empty?
|
|
173
|
+
node = stack.pop
|
|
174
|
+
next unless node.is_a?(Prism::Node)
|
|
175
|
+
|
|
176
|
+
budget -= 1
|
|
177
|
+
return false if budget.negative?
|
|
178
|
+
return true if published_constant_node?(node, scope)
|
|
179
|
+
|
|
180
|
+
node.rigor_each_child { |child| stack << child }
|
|
181
|
+
end
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def published_constant_node?(node, scope)
|
|
186
|
+
case node
|
|
187
|
+
when Prism::ConstantReadNode then scope.published_constant?(node.name.to_s)
|
|
188
|
+
when Prism::ConstantPathNode then published_path?(node, scope)
|
|
189
|
+
else false
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def last_statement(body)
|
|
194
|
+
body.is_a?(Prism::StatementsNode) ? body.body.last : body
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -170,8 +170,7 @@ module Rigor
|
|
|
170
170
|
def extend_prefix(node, prefix)
|
|
171
171
|
return prefix unless CLASS_OR_MODULE_NODE_CLASSES.any? { |klass| node.is_a?(klass) }
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
name ? prefix + [name] : prefix
|
|
173
|
+
Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
|
|
175
174
|
end
|
|
176
175
|
end
|
|
177
176
|
end
|
|
@@ -44,12 +44,12 @@ module Rigor
|
|
|
44
44
|
case node
|
|
45
45
|
when Prism::ModuleNode
|
|
46
46
|
name = Source::ConstantPath.qualified_name_or_nil(node.constant_path)
|
|
47
|
-
child_prefix =
|
|
47
|
+
child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
|
|
48
48
|
names << child_prefix.join("::") if name
|
|
49
49
|
walk(node.body, child_prefix, names) if node.body
|
|
50
50
|
when Prism::ClassNode
|
|
51
51
|
name = Source::ConstantPath.qualified_name_or_nil(node.constant_path)
|
|
52
|
-
child_prefix =
|
|
52
|
+
child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
|
|
53
53
|
names << child_prefix.join("::") if name && class_surface_open?(node)
|
|
54
54
|
walk(node.body, child_prefix, names) if node.body
|
|
55
55
|
else
|
|
@@ -88,8 +88,7 @@ module Rigor
|
|
|
88
88
|
def extend_prefix(node, prefix)
|
|
89
89
|
return prefix unless node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
name ? prefix + [name] : prefix
|
|
91
|
+
Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
|
|
93
92
|
end
|
|
94
93
|
|
|
95
94
|
def collect_begin(node, prefix)
|
|
@@ -160,7 +159,7 @@ module Rigor
|
|
|
160
159
|
name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
161
160
|
return nil if name.nil?
|
|
162
161
|
|
|
163
|
-
return name if
|
|
162
|
+
return name if Source::ConstantPath.rooted?(node)
|
|
164
163
|
|
|
165
164
|
candidates = []
|
|
166
165
|
prefix.size.downto(1) { |i| candidates << (prefix.first(i) + [name]).join("::") }
|
|
@@ -168,15 +167,6 @@ module Rigor
|
|
|
168
167
|
candidates.find { |candidate| known_name?(candidate, scope) }
|
|
169
168
|
end
|
|
170
169
|
|
|
171
|
-
def absolute_path?(node)
|
|
172
|
-
return false unless node.is_a?(Prism::ConstantPathNode)
|
|
173
|
-
|
|
174
|
-
parent = node.parent
|
|
175
|
-
return true if parent.nil? # `::Foo`
|
|
176
|
-
|
|
177
|
-
parent.is_a?(Prism::ConstantPathNode) && absolute_path?(parent)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
170
|
# Presence check used to pick the lexical candidate — deliberately broader than certification, so a
|
|
181
171
|
# name that resolves to a project module is FOUND here and then rejected by {#certified_class?}
|
|
182
172
|
# (rather than mis-resolving outward to a same-named core class).
|
|
@@ -268,6 +258,10 @@ module Rigor
|
|
|
268
258
|
# `Mail::Error`), longest prefix first; falls back to the as-written name (which RBS may know, e.g.
|
|
269
259
|
# `StandardError`).
|
|
270
260
|
def resolve_parent(child, parent, scope)
|
|
261
|
+
# Issue #722 residue 1 — a rooted parent is anchored at the top level; the namespace walk below
|
|
262
|
+
# is Ruby's lexical lookup, which `::` opts out of.
|
|
263
|
+
return parent.delete_prefix("::") if parent.start_with?("::")
|
|
264
|
+
|
|
271
265
|
segments = child.to_s.split("::")[0...-1]
|
|
272
266
|
until segments.empty?
|
|
273
267
|
candidate = "#{segments.join('::')}::#{parent}"
|