rigortype 0.1.19 → 0.2.1
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 +41 -6
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +138 -16
- data/lib/rigor/cli/coverage_command.rb +138 -31
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +57 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +76 -5
- data/lib/rigor/environment.rb +66 -8
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +271 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +180 -0
- data/lib/rigor/protection/mutator.rb +267 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +49 -1
|
@@ -12,6 +12,7 @@ require_relative "method_dispatcher/constant_folding"
|
|
|
12
12
|
require_relative "method_dispatcher/literal_string_folding"
|
|
13
13
|
require_relative "method_dispatcher/shape_dispatch"
|
|
14
14
|
require_relative "method_dispatcher/data_folding"
|
|
15
|
+
require_relative "method_dispatcher/struct_folding"
|
|
15
16
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
16
17
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
17
18
|
require_relative "method_dispatcher/reduce_folding"
|
|
@@ -38,28 +39,15 @@ module Rigor
|
|
|
38
39
|
# callers (today only `ExpressionTyper`) own the fail-soft fallback
|
|
39
40
|
# and decide whether to record a `FallbackTracer` event.
|
|
40
41
|
#
|
|
41
|
-
#
|
|
42
|
+
# Tier order is documented inline in `resolve`; the precise-tier
|
|
43
|
+
# group is built from `PRECISE_TIERS_HEAD`, `STDLIB_SINGLETON_FOLDERS`,
|
|
44
|
+
# and `PRECISE_TIERS_TAIL`. `ShapeDispatch` runs above {RbsDispatch}
|
|
45
|
+
# so a precise per-position/per-key answer wins over the projected
|
|
46
|
+
# `Array#[]`/`Hash#fetch` RBS answer.
|
|
42
47
|
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# 2. {ShapeDispatch}: returns the precise element/value type for a
|
|
47
|
-
# curated catalogue of `Tuple`/`HashShape` element-access
|
|
48
|
-
# methods (`first`, `last`, `[]` with a static integer/key,
|
|
49
|
-
# `fetch`, `dig`, `size`/`length`/`count`). Slice 5 phase 2.
|
|
50
|
-
# 3. {RbsDispatch}: looks up the receiver's class in the RBS
|
|
51
|
-
# environment carried by the scope and translates the method's
|
|
52
|
-
# return type into a Rigor::Type. Slice 4.
|
|
53
|
-
#
|
|
54
|
-
# `ShapeDispatch` deliberately runs *above* {RbsDispatch} so the
|
|
55
|
-
# precise per-position/per-key answer wins over the projected
|
|
56
|
-
# `Array#[]`/`Hash#fetch` answer; it falls through (`nil`) when
|
|
57
|
-
# the call cannot be proved against the static shape, in which
|
|
58
|
-
# case the projection answer from {RbsDispatch} applies.
|
|
59
|
-
#
|
|
60
|
-
# The dispatcher's public signature reserves space for `block_type:`
|
|
61
|
-
# and ADR-2 plugin extensions (later slices), so call sites added
|
|
62
|
-
# now do not have to be rewritten when those tiers arrive.
|
|
48
|
+
# The `block_type:` and plugin contribution (`dynamic_return`) tiers
|
|
49
|
+
# landed in Slice 6 phase C and v0.1.1 Track 2 respectively; all
|
|
50
|
+
# call sites pass through `dispatch`/`resolve` unchanged.
|
|
63
51
|
module MethodDispatcher # rubocop:disable Metrics/ModuleLength
|
|
64
52
|
module_function
|
|
65
53
|
|
|
@@ -326,19 +314,6 @@ module Rigor
|
|
|
326
314
|
Type::Combinator.untyped
|
|
327
315
|
end
|
|
328
316
|
|
|
329
|
-
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
330
|
-
# slice 7; ADR-52 WD3 — consults each loaded plugin's gated
|
|
331
|
-
# `dynamic_return` rules, wraps the contributed types as
|
|
332
|
-
# `FlowContribution` bundles, merges them through
|
|
333
|
-
# `FlowContribution::Merger`, and returns the merged
|
|
334
|
-
# `return_type` slot (or nil when no plugin contributed a
|
|
335
|
-
# return type).
|
|
336
|
-
#
|
|
337
|
-
# Plugins whose hook raises have their contribution
|
|
338
|
-
# silently dropped for this call so the dispatch chain
|
|
339
|
-
# keeps moving — the run-level diagnostic envelope (per
|
|
340
|
-
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
341
|
-
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
342
317
|
# ADR-20 slice 3 — looks up the receiver / method pair
|
|
343
318
|
# in {Rigor::Builtins::HktBuiltins::METHOD_RETURN_OVERRIDES}
|
|
344
319
|
# and returns the reduced HKT type. Only fires when the
|
|
@@ -413,6 +388,19 @@ module Rigor
|
|
|
413
388
|
end
|
|
414
389
|
end
|
|
415
390
|
|
|
391
|
+
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
392
|
+
# slice 7; ADR-52 WD3 — consults each loaded plugin's gated
|
|
393
|
+
# `dynamic_return` rules, wraps the contributed types as
|
|
394
|
+
# `FlowContribution` bundles, merges them through
|
|
395
|
+
# `FlowContribution::Merger`, and returns the merged
|
|
396
|
+
# `return_type` slot (or nil when no plugin contributed a
|
|
397
|
+
# return type).
|
|
398
|
+
#
|
|
399
|
+
# Plugins whose hook raises have their contribution
|
|
400
|
+
# silently dropped for this call so the dispatch chain
|
|
401
|
+
# keeps moving — the run-level diagnostic envelope (per
|
|
402
|
+
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
403
|
+
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
416
404
|
def try_plugin_contribution(call_node, scope, receiver_type)
|
|
417
405
|
return nil if call_node.nil? || scope.nil?
|
|
418
406
|
|
|
@@ -425,16 +413,6 @@ module Rigor
|
|
|
425
413
|
FlowContribution::Merger.merge(contributions).return_type
|
|
426
414
|
end
|
|
427
415
|
|
|
428
|
-
# ADR-10 slice 2b-ii. Consults the per-run
|
|
429
|
-
# `Analysis::DependencySourceInference::Index` carried by
|
|
430
|
-
# the environment for `(class_name, method_name)`
|
|
431
|
-
# observations harvested from opt-in gems' `roots:`. On a
|
|
432
|
-
# hit, returns `Combinator.untyped` so the call site
|
|
433
|
-
# carries the `Dynamic[top]` provenance (per ADR-10's
|
|
434
|
-
# "Inference contract": gem-source-inferred shapes never
|
|
435
|
-
# publish as ground-truth `T`). Returns `nil` when the
|
|
436
|
-
# environment carries no index, the index has no entry, or
|
|
437
|
-
# the receiver has no nominal class to look up.
|
|
438
416
|
# ADR-16 synthetic-method tier. Slice 2b shipped the floor —
|
|
439
417
|
# a match short-circuits at the right precedence (above
|
|
440
418
|
# dep-source / discovered / user-class-fallback; below RBS)
|
|
@@ -556,6 +534,16 @@ module Rigor
|
|
|
556
534
|
Type::Combinator.dynamic(entry.return_type)
|
|
557
535
|
end
|
|
558
536
|
|
|
537
|
+
# ADR-10 slice 2b-ii. Consults the per-run
|
|
538
|
+
# `Analysis::DependencySourceInference::Index` carried by
|
|
539
|
+
# the environment for `(class_name, method_name)`
|
|
540
|
+
# observations harvested from opt-in gems' `roots:`. On a
|
|
541
|
+
# hit, returns `Combinator.untyped` so the call site
|
|
542
|
+
# carries the `Dynamic[top]` provenance (per ADR-10's
|
|
543
|
+
# "Inference contract": gem-source-inferred shapes never
|
|
544
|
+
# publish as ground-truth `T`). Returns `nil` when the
|
|
545
|
+
# environment carries no index, the index has no entry, or
|
|
546
|
+
# the receiver has no nominal class to look up.
|
|
559
547
|
def try_dependency_source(receiver_type, method_name, environment)
|
|
560
548
|
index = environment&.dependency_source_index
|
|
561
549
|
return nil if index.nil? || index.empty?
|
|
@@ -759,10 +747,6 @@ module Rigor
|
|
|
759
747
|
# below this chain and is invoked by the outer `dispatch`
|
|
760
748
|
# method.
|
|
761
749
|
#
|
|
762
|
-
# `BlockFolding` runs last among the precision tiers because
|
|
763
|
-
# its rules apply only to block-taking calls, so the cheaper
|
|
764
|
-
# arity-based fold tiers above it filter out the common
|
|
765
|
-
# cases first. When `block_type` is nil the tier is a no-op.
|
|
766
750
|
# The precise-tier folders, consulted in order via the uniform
|
|
767
751
|
# `_DispatchTier` interface (`try_dispatch(CallContext) -> Type?`).
|
|
768
752
|
# Order is significant: ConstantFolding's exact-value folds win
|
|
@@ -815,6 +799,14 @@ module Rigor
|
|
|
815
799
|
data_result = DataFolding.try_dispatch(context)
|
|
816
800
|
return data_result if data_result
|
|
817
801
|
|
|
802
|
+
# ADR-48 Struct follow-up — runs in the same band and for the same
|
|
803
|
+
# reason as DataFolding: `meta_new` would otherwise intercept every
|
|
804
|
+
# `Singleton[*].new` (the old `struct_new_lift` produced a bare
|
|
805
|
+
# `Singleton[Struct]`), masking a Struct class's precise instance.
|
|
806
|
+
# Only fires on Struct receivers, so it never shadows meta's lifts.
|
|
807
|
+
struct_result = StructFolding.try_dispatch(context)
|
|
808
|
+
return struct_result if struct_result
|
|
809
|
+
|
|
818
810
|
meta_result = try_meta_introspection(context.receiver, context.method_name, context.args)
|
|
819
811
|
return meta_result if meta_result
|
|
820
812
|
|
|
@@ -67,18 +67,12 @@ module Rigor
|
|
|
67
67
|
# ...; @warning_issued = true`). Requires tracking the
|
|
68
68
|
# first-write position; flow-sensitive but orthogonal.
|
|
69
69
|
# - **Local-variable mutation inside a block body** (e.g.
|
|
70
|
-
# `arr = []; xs.each { |x| arr << x }`)
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# an outer LOCAL does not yet flow back. **Ivar mutations
|
|
75
|
-
# inside a block ARE handled** (ivars live in the
|
|
76
|
-
# method-body scope, not the block-local scope) — the
|
|
77
|
-
# widening fires from inside the block and the new ivar
|
|
78
|
-
# binding is visible to the outer scope.
|
|
70
|
+
# `arr = []; xs.each { |x| arr << x }`) — landed as
|
|
71
|
+
# ADR-56 slice A (`widen_after_block`). **Ivar mutations
|
|
72
|
+
# inside a block ARE also handled** (ivars live in the
|
|
73
|
+
# method-body scope, not the block-local scope).
|
|
79
74
|
#
|
|
80
|
-
#
|
|
81
|
-
# `docs/CURRENT_WORK.md` and are intentionally deferred.
|
|
75
|
+
# The remaining three items above are demand-gated; see ADR-56.
|
|
82
76
|
module MutationWidening
|
|
83
77
|
# Array mutators that change either the size or the element
|
|
84
78
|
# set of a literal-shape carrier (Tuple). Receiver-mutating
|
|
@@ -11,7 +11,8 @@ require_relative "../builtins/regex_refinement"
|
|
|
11
11
|
|
|
12
12
|
module Rigor
|
|
13
13
|
module Inference
|
|
14
|
-
#
|
|
14
|
+
# Control-flow predicate narrowing and type-lattice narrowing
|
|
15
|
+
# primitives.
|
|
15
16
|
#
|
|
16
17
|
# `Rigor::Inference::Narrowing` answers two related questions:
|
|
17
18
|
#
|
|
@@ -19,22 +20,19 @@ module Rigor
|
|
|
19
20
|
# truthy fragment, its falsey fragment, its nil fragment, and its
|
|
20
21
|
# non-nil fragment? These primitives understand the value-lattice
|
|
21
22
|
# algebra (`Constant`, `Nominal`, `Singleton`, `Tuple`, `HashShape`,
|
|
22
|
-
# `Union`) and stay conservative on `Top` and `Dynamic[T]
|
|
23
|
-
# the analyzer cannot prove the boundary either way.
|
|
23
|
+
# `Union`) and stay conservative on `Top` and `Dynamic[T]`.
|
|
24
24
|
# 2. Predicate-level narrowing: given a Prism predicate node and an
|
|
25
25
|
# entry scope, what are the truthy-edge scope and the falsey-edge
|
|
26
|
-
# scope
|
|
27
|
-
#
|
|
28
|
-
# against
|
|
29
|
-
#
|
|
26
|
+
# scope? The catalogue covers truthiness, `nil?`, `!`, `&&`/`||`,
|
|
27
|
+
# class-membership (`is_a?`, `kind_of?`, `instance_of?`), trusted
|
|
28
|
+
# equality/inequality against static literals, `case`/`when`,
|
|
29
|
+
# regex match globals, string predicates (`start_with?` etc.),
|
|
30
|
+
# key-presence, array emptiness, numeric comparison, and
|
|
31
|
+
# `respond_to?`.
|
|
30
32
|
#
|
|
31
|
-
#
|
|
32
|
-
# `
|
|
33
|
-
# `
|
|
34
|
-
# bindings on truthiness and `nil?`; phase 2 extends the catalogue
|
|
35
|
-
# with class-membership predicates (`is_a?`, `kind_of?`,
|
|
36
|
-
# `instance_of?`) and trusted equality/inequality checks against
|
|
37
|
-
# static literals.
|
|
33
|
+
# Consumed by `Rigor::Inference::StatementEvaluator` to refine
|
|
34
|
+
# `then`/`else` scopes of `IfNode`/`UnlessNode` and
|
|
35
|
+
# `case`/`when` branches.
|
|
38
36
|
#
|
|
39
37
|
# The module is pure: every public function returns fresh values and
|
|
40
38
|
# MUST NOT mutate its inputs. Unrecognised predicate shapes degrade
|
|
@@ -43,8 +41,8 @@ module Rigor
|
|
|
43
41
|
# `[truthy_scope, falsey_scope]` pair (the entry scope twice when no
|
|
44
42
|
# rule matches).
|
|
45
43
|
#
|
|
46
|
-
# See docs/internal-spec/inference-engine.md (
|
|
47
|
-
#
|
|
44
|
+
# See docs/internal-spec/inference-engine.md (Narrowing) and
|
|
45
|
+
# docs/type-specification/control-flow-analysis.md for the
|
|
48
46
|
# binding contract.
|
|
49
47
|
# rubocop:disable Metrics/ModuleLength
|
|
50
48
|
module Narrowing
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "scope_indexer"
|
|
6
|
+
require_relative "../source/node_walker"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Pure argument-type classification for {ParameterInferenceCollector} — no
|
|
11
|
+
# collector state, so it lives outside the orchestration class. Decides which
|
|
12
|
+
# argument types may seed a parameter (concrete enough for the protection
|
|
13
|
+
# metric to bite) and widens a literal argument to its nominal.
|
|
14
|
+
module ParameterArgTypes
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
CONSTANT_CLASSES = {
|
|
18
|
+
Integer => "Integer", Float => "Float", String => "String",
|
|
19
|
+
Symbol => "Symbol", Range => "Range", TrueClass => "TrueClass",
|
|
20
|
+
FalseClass => "FalseClass", NilClass => "NilClass"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# A parameter holds a *value of* a type across its lifetime, not a pinned
|
|
24
|
+
# literal — so a `Constant<"text">` argument widens to its nominal
|
|
25
|
+
# (`String`); recurses through unions so `Constant<"a"> | Constant<"b">`
|
|
26
|
+
# collapses to `String`.
|
|
27
|
+
def widen_for_param(type)
|
|
28
|
+
case type
|
|
29
|
+
when Type::Constant
|
|
30
|
+
name = constant_class_name(type.value)
|
|
31
|
+
name ? Type::Combinator.nominal_of(name) : type
|
|
32
|
+
when Type::Union
|
|
33
|
+
Type::Combinator.union(*type.members.map { |member| widen_for_param(member) })
|
|
34
|
+
else
|
|
35
|
+
type
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def constant_class_name(value)
|
|
40
|
+
CONSTANT_CLASSES.each { |klass, name| return name if value.is_a?(klass) }
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The dispatch class for a receiver type, for the subset the collector
|
|
45
|
+
# resolves to a user `def` (mirrors `CheckRules#concrete_class_name` for the
|
|
46
|
+
# carriers a user-method receiver is typed as). `class_name_of` is the
|
|
47
|
+
# Nominal/Singleton-only variant for an implicit-self `self_type`.
|
|
48
|
+
def concrete_class_name(type)
|
|
49
|
+
case type
|
|
50
|
+
when Type::Nominal, Type::Singleton then type.class_name
|
|
51
|
+
when Type::Tuple then "Array"
|
|
52
|
+
when Type::HashShape then "Hash"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def class_name_of(type)
|
|
57
|
+
type.class_name if type.is_a?(Type::Nominal) || type.is_a?(Type::Singleton)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Whether `type` is too gradual to seed (not a concrete dispatch target) —
|
|
61
|
+
# the negation of `ProtectionScanner#concrete_receiver?` (a union is
|
|
62
|
+
# concrete only when every arm is).
|
|
63
|
+
def non_concrete?(type)
|
|
64
|
+
case type
|
|
65
|
+
when Type::Dynamic, Type::Top, Type::Bot then true
|
|
66
|
+
when Type::Union then type.members.any? { |member| non_concrete?(member) }
|
|
67
|
+
else false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ADR-67 WD3 + WD5 — call-site parameter type inference (capped fixpoint).
|
|
73
|
+
#
|
|
74
|
+
# A `def` parameter with no RBS signature types `untyped` (the gradual entry
|
|
75
|
+
# point), so a param flowing into an ivar or a receiver drains everything
|
|
76
|
+
# downstream to `Dynamic`. This pass closes that hole: it walks every project
|
|
77
|
+
# file, types each call's positional arguments in their lexical scope,
|
|
78
|
+
# resolves which user-defined `def` each call targets, and records the
|
|
79
|
+
# **union of resolved call-site argument types** per parameter — TypeProf's
|
|
80
|
+
# "a parameter's type is the union of every actual argument across all call
|
|
81
|
+
# sites". WD5: it iterates (cap {DEFAULT_ROUNDS}), re-seeding each round with
|
|
82
|
+
# the previous round's inferred parameters, so a parameter passed *another*
|
|
83
|
+
# parameter is typed one hop further per round (round 1 alone is the
|
|
84
|
+
# single-level pass).
|
|
85
|
+
#
|
|
86
|
+
# The result is keyed by `[class_name, method_name, kind]` — the same triple
|
|
87
|
+
# `StatementEvaluator#build_method_entry_scope` reconstructs from the lexical
|
|
88
|
+
# class path — so the consumer can seed an undeclared parameter with its
|
|
89
|
+
# inferred type. The table is precision-additive only: it never feeds a
|
|
90
|
+
# parameter-boundary diagnostic (an inferred type lives solely as a body
|
|
91
|
+
# local, and the boundary rules consult RBS, not body locals — see ADR-67
|
|
92
|
+
# WD1), so being wrong cannot manufacture a false positive at a caller.
|
|
93
|
+
#
|
|
94
|
+
# Soundness (WD4): the inferred type is a sound over-approximation only when
|
|
95
|
+
# every contributed call site resolves to a concrete argument type. Any
|
|
96
|
+
# `Dynamic` / `Top` / `Bot` argument (a `send`/dynamic-dispatch caller, or a
|
|
97
|
+
# parameter not yet typed in an earlier round) **poisons** the parameter,
|
|
98
|
+
# which then contributes nothing this round (it may type in a later round
|
|
99
|
+
# once its own argument resolves). Unresolved call sites do not contribute.
|
|
100
|
+
#
|
|
101
|
+
# What it does NOT do yet (deferred — the check-wiring slice): feeding the
|
|
102
|
+
# table into the `check` walk (only `coverage --protection` consumes it
|
|
103
|
+
# today), keyword / optional / rest / block parameters, inherited-method
|
|
104
|
+
# receivers, top-level helpers, and a rigorous closed-call-site-set proof
|
|
105
|
+
# (the union is optimistic over resolved sites — acceptable because the only
|
|
106
|
+
# consumer is the protection metric, which runs no diagnostics).
|
|
107
|
+
class ParameterInferenceCollector
|
|
108
|
+
EMPTY = {}.freeze
|
|
109
|
+
|
|
110
|
+
# A defensive widening cap (ADR-41): a parameter unioned from more than
|
|
111
|
+
# this many distinct concrete call-site types is widened to `untyped`
|
|
112
|
+
# (poisoned) rather than carrying an unbounded union.
|
|
113
|
+
MAX_CALL_SITE_TYPES = 16
|
|
114
|
+
|
|
115
|
+
# WD5 — the call-site union is a worklist fixpoint: each round re-types the
|
|
116
|
+
# project with the previous round's inferred parameters seeded, so a
|
|
117
|
+
# parameter passed *another* parameter is typed one hop further per round.
|
|
118
|
+
# Capped (no true-convergence requirement — the metric tolerates a bounded
|
|
119
|
+
# approximation, and the table can oscillate at the margin since a newly
|
|
120
|
+
# resolved receiver can surface a fresh untyped-argument call site). The
|
|
121
|
+
# cap matches the `Inference::BodyFixpoint` cap-3 convention. Round 1 alone
|
|
122
|
+
# is the single-level pass.
|
|
123
|
+
DEFAULT_ROUNDS = 3
|
|
124
|
+
|
|
125
|
+
# @param files [Array<String>] project `.rb` paths to scan for call sites.
|
|
126
|
+
# @param environment [Rigor::Environment]
|
|
127
|
+
# @param target_ruby [String, nil] Prism parse target.
|
|
128
|
+
# @param max_rounds [Integer] the WD5 fixpoint cap (1 = single-level).
|
|
129
|
+
# @return [Hash{[String,Symbol,Symbol] => Hash{Symbol => Rigor::Type}}] frozen.
|
|
130
|
+
def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS)
|
|
131
|
+
new(files: files, environment: environment, target_ruby: target_ruby, max_rounds: max_rounds).collect
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def initialize(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS)
|
|
135
|
+
@files = files
|
|
136
|
+
@environment = environment
|
|
137
|
+
@target_ruby = target_ruby
|
|
138
|
+
@max_rounds = max_rounds
|
|
139
|
+
# Reset per round (see {#run_round}). `[[class, method, kind], param_sym]`
|
|
140
|
+
# => [Type] of observed concrete arguments (a default-block Hash, not a
|
|
141
|
+
# `{}` literal, so the analyzer types its reads generically — {#finalize}),
|
|
142
|
+
# plus the ids widened to `untyped` (an untyped / over-cap argument).
|
|
143
|
+
@type_observations = Hash.new { |hash, id| hash[id] = [] }
|
|
144
|
+
@poisoned_params = Set.new
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def collect
|
|
148
|
+
parsed = parse_all
|
|
149
|
+
discovery = discovery_seed_tables
|
|
150
|
+
table = EMPTY
|
|
151
|
+
@max_rounds.times do
|
|
152
|
+
rounded = run_round(parsed, discovery, table)
|
|
153
|
+
return rounded if rounded == table # fixpoint reached
|
|
154
|
+
|
|
155
|
+
table = rounded
|
|
156
|
+
end
|
|
157
|
+
table
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
# Parse every project file once; rounds re-index the cached ASTs against an
|
|
163
|
+
# evolving seed rather than re-parsing.
|
|
164
|
+
def parse_all
|
|
165
|
+
@files.filter_map do |path|
|
|
166
|
+
result = Prism.parse(File.read(path), filepath: path, version: @target_ruby)
|
|
167
|
+
[path, result.value] if result.errors.empty?
|
|
168
|
+
rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# One fixpoint round: re-type every file with `seed_table` (the previous
|
|
174
|
+
# round's inferred parameters) seeded, collecting the next round's table.
|
|
175
|
+
def run_round(parsed, discovery_tables, seed_table)
|
|
176
|
+
@type_observations = Hash.new { |hash, id| hash[id] = [] }
|
|
177
|
+
@poisoned_params = Set.new
|
|
178
|
+
seed_scope = build_seed_scope(discovery_tables, seed_table)
|
|
179
|
+
parsed.each do |path, ast|
|
|
180
|
+
index = ScopeIndexer.index(ast, default_scope: seed_scope.with_source_path(path))
|
|
181
|
+
Source::NodeWalker.each(ast) do |node|
|
|
182
|
+
record_call(node, index) if node.is_a?(Prism::CallNode)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
finalize
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# A scope carrying the cross-file discovery index (so `Foo.new` receivers
|
|
189
|
+
# and implicit-self calls resolve to a user `def`) plus the prior round's
|
|
190
|
+
# inferred parameters (so an argument that reads a parameter types to its
|
|
191
|
+
# current inferred type — the WD5 propagation).
|
|
192
|
+
def build_seed_scope(discovery_tables, seed_table)
|
|
193
|
+
base = Scope.empty(environment: @environment)
|
|
194
|
+
tables = discovery_tables
|
|
195
|
+
tables = tables.merge(param_inferred_types: seed_table) unless seed_table.empty?
|
|
196
|
+
return base if tables.empty?
|
|
197
|
+
|
|
198
|
+
base.with_discovery(base.discovery.with(**tables))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def discovery_seed_tables
|
|
202
|
+
classes = ScopeIndexer.discovered_classes_for_paths(@files)
|
|
203
|
+
def_index = ScopeIndexer.discovered_def_index_for_paths(@files)
|
|
204
|
+
tables = {}
|
|
205
|
+
tables[:discovered_classes] = classes unless classes.empty?
|
|
206
|
+
DISCOVERY_FIELD.each do |index_key, field|
|
|
207
|
+
table = def_index.fetch(index_key)
|
|
208
|
+
tables[field] = table unless table.empty?
|
|
209
|
+
end
|
|
210
|
+
tables
|
|
211
|
+
rescue StandardError
|
|
212
|
+
# Discovery is best-effort; a malformed corner of the project must not
|
|
213
|
+
# crash the protection scan. Without discovery the collector simply
|
|
214
|
+
# resolves fewer call sites.
|
|
215
|
+
{}
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
DISCOVERY_FIELD = {
|
|
219
|
+
def_nodes: :discovered_def_nodes,
|
|
220
|
+
singleton_def_nodes: :discovered_singleton_def_nodes,
|
|
221
|
+
def_sources: :discovered_def_sources,
|
|
222
|
+
superclasses: :discovered_superclasses,
|
|
223
|
+
includes: :discovered_includes,
|
|
224
|
+
class_sources: :discovered_class_sources,
|
|
225
|
+
method_visibilities: :discovered_method_visibilities,
|
|
226
|
+
methods: :discovered_methods,
|
|
227
|
+
data_member_layouts: :data_member_layouts,
|
|
228
|
+
struct_member_layouts: :struct_member_layouts
|
|
229
|
+
}.freeze
|
|
230
|
+
private_constant :DISCOVERY_FIELD
|
|
231
|
+
|
|
232
|
+
def record_call(call_node, index)
|
|
233
|
+
args = positional_args(call_node)
|
|
234
|
+
return if args.nil?
|
|
235
|
+
|
|
236
|
+
scope = index[call_node]
|
|
237
|
+
return if scope.nil?
|
|
238
|
+
|
|
239
|
+
callee = resolve_callee(call_node, scope, index)
|
|
240
|
+
return if callee.nil?
|
|
241
|
+
|
|
242
|
+
class_name, method, kind, def_node = callee
|
|
243
|
+
requireds = simple_requireds(def_node)
|
|
244
|
+
return if requireds.nil? || requireds.size != args.size
|
|
245
|
+
|
|
246
|
+
key = [class_name, method, kind]
|
|
247
|
+
args.each_with_index do |arg, i|
|
|
248
|
+
arg_scope = index[arg]
|
|
249
|
+
accumulate(key, requireds[i].name, arg_scope&.type_of(arg))
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# The plain positional arguments, or nil when the call carries any
|
|
254
|
+
# non-plain argument (splat / keyword / block-pass / forwarding) — those
|
|
255
|
+
# break the positional-index ↔ parameter mapping, so the call site is
|
|
256
|
+
# skipped rather than mis-attributed.
|
|
257
|
+
def positional_args(call_node)
|
|
258
|
+
arguments = call_node.arguments
|
|
259
|
+
return [] if arguments.nil?
|
|
260
|
+
|
|
261
|
+
list = arguments.arguments
|
|
262
|
+
return nil if list.any? { |arg| non_plain_argument?(arg) }
|
|
263
|
+
|
|
264
|
+
list
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def non_plain_argument?(arg)
|
|
268
|
+
arg.is_a?(Prism::SplatNode) ||
|
|
269
|
+
arg.is_a?(Prism::KeywordHashNode) ||
|
|
270
|
+
arg.is_a?(Prism::BlockArgumentNode) ||
|
|
271
|
+
arg.is_a?(Prism::ForwardingArgumentsNode) ||
|
|
272
|
+
arg.is_a?(Prism::AssocNode) ||
|
|
273
|
+
arg.is_a?(Prism::AssocSplatNode)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# @return [[String, Symbol, Symbol, Prism::DefNode], nil]
|
|
277
|
+
def resolve_callee(call_node, scope, index)
|
|
278
|
+
if call_node.receiver.nil?
|
|
279
|
+
class_name, kind = implicit_self_target(scope)
|
|
280
|
+
else
|
|
281
|
+
class_name, kind = explicit_receiver_target(call_node.receiver, index, scope)
|
|
282
|
+
end
|
|
283
|
+
return nil if class_name.nil?
|
|
284
|
+
|
|
285
|
+
def_node = lookup_def(scope, class_name, call_node.name, kind)
|
|
286
|
+
return nil if def_node.nil?
|
|
287
|
+
|
|
288
|
+
[class_name, call_node.name, kind, def_node]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def implicit_self_target(scope)
|
|
292
|
+
self_type = scope.self_type
|
|
293
|
+
return [nil, nil] if self_type.nil?
|
|
294
|
+
|
|
295
|
+
[ParameterArgTypes.class_name_of(self_type), self_type.is_a?(Type::Singleton) ? :singleton : :instance]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def explicit_receiver_target(receiver, index, scope)
|
|
299
|
+
receiver_type = (index[receiver] || scope).type_of(receiver)
|
|
300
|
+
[ParameterArgTypes.concrete_class_name(receiver_type),
|
|
301
|
+
receiver_type.is_a?(Type::Singleton) ? :singleton : :instance]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def lookup_def(scope, class_name, method, kind)
|
|
305
|
+
table = kind == :singleton ? scope.discovered_singleton_def_nodes : scope.discovered_def_nodes
|
|
306
|
+
per_class = table[class_name]
|
|
307
|
+
per_class && per_class[method]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# The required-positional parameters, or nil when the method's parameter
|
|
311
|
+
# list is not a simple all-required shape (matching the single-level
|
|
312
|
+
# contract `ExpressionTyper#user_method_param_shape_simple?` uses) or
|
|
313
|
+
# contains a destructured `(a, b)` slot (no bindable name).
|
|
314
|
+
def simple_requireds(def_node)
|
|
315
|
+
params = def_node.parameters
|
|
316
|
+
return [] if params.nil?
|
|
317
|
+
return nil unless params.is_a?(Prism::ParametersNode)
|
|
318
|
+
return nil unless params.optionals.empty? && params.rest.nil? && params.posts.empty? &&
|
|
319
|
+
params.keywords.empty? && params.keyword_rest.nil? && params.block.nil?
|
|
320
|
+
return nil unless params.requireds.all?(Prism::RequiredParameterNode)
|
|
321
|
+
|
|
322
|
+
params.requireds
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def accumulate(key, param_name, arg_type)
|
|
326
|
+
id = [key, param_name.to_sym]
|
|
327
|
+
return if @poisoned_params.include?(id)
|
|
328
|
+
|
|
329
|
+
if arg_type.nil? || ParameterArgTypes.non_concrete?(arg_type)
|
|
330
|
+
poison(id)
|
|
331
|
+
else
|
|
332
|
+
observations = @type_observations[id]
|
|
333
|
+
observations << ParameterArgTypes.widen_for_param(arg_type)
|
|
334
|
+
poison(id) if observations.length > MAX_CALL_SITE_TYPES
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# A poisoned parameter is dropped from the observation store and recorded
|
|
339
|
+
# so later call sites short-circuit. `id` is the `[[class, method, kind],
|
|
340
|
+
# param]` pair.
|
|
341
|
+
def poison(id)
|
|
342
|
+
@poisoned_params << id
|
|
343
|
+
@type_observations.delete(id)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def finalize
|
|
347
|
+
# `result` is a default-block Hash (not a `{}` literal) so the analyzer
|
|
348
|
+
# types its reads generically rather than folding the empty shape — the
|
|
349
|
+
# nesting writes stay plain assignments, no literal-fold conditions.
|
|
350
|
+
result = Hash.new { |hash, key| hash[key] = {} }
|
|
351
|
+
@type_observations.each do |id, observations|
|
|
352
|
+
next if @poisoned_params.include?(id)
|
|
353
|
+
next if observations.empty?
|
|
354
|
+
|
|
355
|
+
union = Type::Combinator.union(*observations)
|
|
356
|
+
# A union that collapsed to a non-concrete shape (e.g. a gradual arm
|
|
357
|
+
# leaked in) is no better than `untyped`; drop it.
|
|
358
|
+
next if ParameterArgTypes.non_concrete?(union)
|
|
359
|
+
|
|
360
|
+
key, param = id
|
|
361
|
+
result[key][param] = union
|
|
362
|
+
end
|
|
363
|
+
result.transform_values(&:freeze).freeze
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
@@ -13,13 +13,10 @@ module Rigor
|
|
|
13
13
|
# project-side `lib/core_ext/string_extensions.rb` patches
|
|
14
14
|
# are visible to cross-file dispatch.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# real-world `core_ext` patches return shapes the analyzer
|
|
21
|
-
# could heuristically extract via the same machinery the
|
|
22
|
-
# ADR-10 walker uses, but slice 2 keeps the surface narrow).
|
|
16
|
+
# The dispatcher answers `Dynamic[T]` (with a heuristic static
|
|
17
|
+
# facet) when `Entry#return_type` is non-nil, or `Dynamic[Top]`
|
|
18
|
+
# when the heuristic declined (`nil`). See {Entry} for the
|
|
19
|
+
# per-field contract.
|
|
23
20
|
class ProjectPatchedMethods
|
|
24
21
|
# Frozen value-object recording one `def` observed by the
|
|
25
22
|
# pre-pass. `class_name` is the qualified prefix
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "project_patched_methods"
|
|
6
6
|
require_relative "../analysis/dependency_source_inference/return_type_heuristic"
|
|
7
|
+
require_relative "../source/constant_path"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Inference
|
|
@@ -161,7 +162,7 @@ module Rigor
|
|
|
161
162
|
private_class_method :walk_children
|
|
162
163
|
|
|
163
164
|
def descend_class_or_module(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
164
|
-
name =
|
|
165
|
+
name = Source::ConstantPath.qualified_name_or_nil(node.constant_path)
|
|
165
166
|
if name && node.body
|
|
166
167
|
walk_node(node.body, qualified_prefix + [name], in_singleton_class, source_path, entries)
|
|
167
168
|
else
|
|
@@ -193,18 +194,6 @@ module Rigor
|
|
|
193
194
|
)
|
|
194
195
|
end
|
|
195
196
|
private_class_method :record_def_node
|
|
196
|
-
|
|
197
|
-
def qualified_name_for(node)
|
|
198
|
-
case node
|
|
199
|
-
when Prism::ConstantReadNode then node.name.to_s
|
|
200
|
-
when Prism::ConstantPathNode
|
|
201
|
-
parent = node.parent.nil? ? nil : qualified_name_for(node.parent)
|
|
202
|
-
return nil if !node.parent.nil? && parent.nil?
|
|
203
|
-
|
|
204
|
-
parent.nil? ? node.name.to_s : "#{parent}::#{node.name}"
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
private_class_method :qualified_name_for
|
|
208
197
|
end
|
|
209
198
|
end
|
|
210
199
|
end
|