rigortype 0.1.18 → 0.2.0
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- 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/diagnostic.rb +8 -0
- 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 +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -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 +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -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 +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -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 +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- 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 +74 -5
- data/lib/rigor/environment.rb +17 -7
- 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/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- 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 +1072 -71
- 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/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- 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/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- 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 +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- 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 +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -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 +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- 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 +18 -49
- 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 +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- 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 +22 -35
- 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 +16 -23
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- 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 +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- 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 +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -58,11 +58,10 @@ module Rigor
|
|
|
58
58
|
# slices route real producers through it.
|
|
59
59
|
# @param workers [Integer] ADR-15 Phase 4b — when greater
|
|
60
60
|
# than zero, per-file analysis dispatches across a pool of
|
|
61
|
-
# N
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
# leaves the parameter as a programmatic opt-in only.
|
|
61
|
+
# N workers. Default `0` keeps the sequential code path
|
|
62
|
+
# bit-for-bit unchanged. Controlled via the
|
|
63
|
+
# `RIGOR_RACTOR_WORKERS` env var or `.rigor.yml`
|
|
64
|
+
# `parallel.workers:` (Phase 4c, fully wired).
|
|
66
65
|
# @param collect_stats [Boolean] when true (default), `#run`
|
|
67
66
|
# builds a {RunStats} summary exposed via `result.stats`
|
|
68
67
|
# — this forces the RBS env build at end-of-run so the
|
|
@@ -155,6 +154,7 @@ module Rigor
|
|
|
155
154
|
@cached_plugin_prepare_diagnostics = [].freeze
|
|
156
155
|
@project_discovered_classes = {}.freeze
|
|
157
156
|
@project_discovered_def_nodes = {}.freeze
|
|
157
|
+
@project_discovered_singleton_def_nodes = {}.freeze
|
|
158
158
|
@project_discovered_def_sources = {}.freeze
|
|
159
159
|
@project_discovered_superclasses = {}.freeze
|
|
160
160
|
@project_discovered_includes = {}.freeze
|
|
@@ -162,6 +162,7 @@ module Rigor
|
|
|
162
162
|
@project_discovered_method_visibilities = {}.freeze
|
|
163
163
|
@project_discovered_methods = {}.freeze
|
|
164
164
|
@project_data_member_layouts = {}.freeze
|
|
165
|
+
@project_struct_member_layouts = {}.freeze
|
|
165
166
|
build_collaborators
|
|
166
167
|
end
|
|
167
168
|
|
|
@@ -457,7 +458,7 @@ module Rigor
|
|
|
457
458
|
# discovery tables at their frozen-empty constructor defaults
|
|
458
459
|
# (the bundle carries `nil` for them, matching the original
|
|
459
460
|
# adopt path that never touched them).
|
|
460
|
-
def apply_pre_passes_result(result)
|
|
461
|
+
def apply_pre_passes_result(result) # rubocop:disable Metrics/AbcSize
|
|
461
462
|
@plugin_registry = result.plugin_registry
|
|
462
463
|
@dependency_source_index = result.dependency_source_index
|
|
463
464
|
@cached_plugin_prepare_diagnostics = result.cached_plugin_prepare_diagnostics
|
|
@@ -466,6 +467,9 @@ module Rigor
|
|
|
466
467
|
@pre_eval_diagnostics_from_scanner = result.pre_eval_diagnostics_from_scanner
|
|
467
468
|
@project_discovered_classes = result.discovered_classes if result.discovered_classes
|
|
468
469
|
@project_discovered_def_nodes = result.discovered_def_nodes if result.discovered_def_nodes
|
|
470
|
+
if result.discovered_singleton_def_nodes
|
|
471
|
+
@project_discovered_singleton_def_nodes = result.discovered_singleton_def_nodes
|
|
472
|
+
end
|
|
469
473
|
@project_discovered_def_sources = result.discovered_def_sources if result.discovered_def_sources
|
|
470
474
|
@project_discovered_superclasses = result.discovered_superclasses if result.discovered_superclasses
|
|
471
475
|
@project_discovered_includes = result.discovered_includes if result.discovered_includes
|
|
@@ -475,6 +479,7 @@ module Rigor
|
|
|
475
479
|
end
|
|
476
480
|
@project_discovered_methods = result.discovered_methods if result.discovered_methods
|
|
477
481
|
@project_data_member_layouts = result.data_member_layouts if result.data_member_layouts
|
|
482
|
+
@project_struct_member_layouts = result.struct_member_layouts if result.struct_member_layouts
|
|
478
483
|
end
|
|
479
484
|
private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result
|
|
480
485
|
|
|
@@ -636,30 +641,44 @@ module Rigor
|
|
|
636
641
|
# `#diagnostics_for_file` or declared a `node_rule` are visited
|
|
637
642
|
# (`contribution_index.for_file_diagnostics`); a skipped plugin's
|
|
638
643
|
# two hooks could only have returned `[]`.
|
|
639
|
-
def plugin_emitted_diagnostics(path, root, scope)
|
|
644
|
+
def plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
640
645
|
return [] if @plugin_registry.empty?
|
|
641
646
|
|
|
642
|
-
# ADR-52 WD4 — one engine-owned AST walk per file dispatches each
|
|
643
|
-
# node to every matching (plugin, rule); the per-plugin results
|
|
644
|
-
# are bucketed in registry order so emission stays plugin-major
|
|
645
|
-
# (byte-identical with the old per-plugin walk).
|
|
646
|
-
node_results = node_rule_results_by_plugin(path, root, scope)
|
|
647
|
-
|
|
648
647
|
@plugin_registry.contribution_index.for_file_diagnostics.flat_map do |plugin|
|
|
649
648
|
collect_plugin_diagnostics(plugin, path, root, scope, node_results[plugin])
|
|
650
649
|
end
|
|
651
650
|
end
|
|
652
651
|
|
|
653
|
-
|
|
652
|
+
# ADR-52 WD4 + ADR-53 B4 — one engine-owned AST walk per file
|
|
653
|
+
# dispatches each node to every matching (plugin, rule) AND drives
|
|
654
|
+
# the built-in node collectors (`node_collectors`), so the file is
|
|
655
|
+
# walked once for both. The per-plugin results are bucketed in
|
|
656
|
+
# registry order so plugin emission stays plugin-major
|
|
657
|
+
# (byte-identical with the old per-plugin walk); the collectors are
|
|
658
|
+
# populated in place for `diagnose` to consume.
|
|
659
|
+
#
|
|
660
|
+
# When no plugin declares a node rule, the walk still runs to drive
|
|
661
|
+
# the collectors (the converged path replaces the standalone
|
|
662
|
+
# `RuleWalk.run`); `node_collectors` nil means a caller that does
|
|
663
|
+
# not need built-in collection from this walk.
|
|
664
|
+
def node_rule_results_by_plugin(path, root, scope, node_collectors, scope_index)
|
|
654
665
|
walk = @plugin_registry.node_rule_walk
|
|
655
|
-
|
|
666
|
+
driver = node_collectors && CheckRules.node_collector_driver(node_collectors)
|
|
667
|
+
return {}.compare_by_identity if walk.empty? && driver.nil?
|
|
656
668
|
|
|
657
|
-
results = walk.diagnostics_for_file(
|
|
669
|
+
results = walk.diagnostics_for_file(
|
|
670
|
+
path: path, scope: scope, root: root, collector_driver: driver
|
|
671
|
+
)
|
|
672
|
+
CheckRules.shadow_verify_converged_collectors(path, root, scope_index, node_collectors) if shadow_rule_walk?
|
|
658
673
|
results.each_with_object({}.compare_by_identity) do |result, by_plugin|
|
|
659
674
|
by_plugin[result.plugin] = result
|
|
660
675
|
end
|
|
661
676
|
end
|
|
662
677
|
|
|
678
|
+
def shadow_rule_walk?
|
|
679
|
+
ENV.fetch("RIGOR_SHADOW_RULE_WALK", nil)
|
|
680
|
+
end
|
|
681
|
+
|
|
663
682
|
def collect_plugin_diagnostics(plugin, path, root, scope, node_result)
|
|
664
683
|
raw = Array(plugin.diagnostics_for_file(path: path, scope: scope, root: root))
|
|
665
684
|
# A node-rule context/rule raise isolates the whole plugin's
|
|
@@ -807,6 +826,9 @@ module Rigor
|
|
|
807
826
|
tables = {}
|
|
808
827
|
tables[:discovered_classes] = @project_discovered_classes unless @project_discovered_classes.empty?
|
|
809
828
|
tables[:discovered_def_nodes] = @project_discovered_def_nodes unless @project_discovered_def_nodes.empty?
|
|
829
|
+
unless @project_discovered_singleton_def_nodes.empty?
|
|
830
|
+
tables[:discovered_singleton_def_nodes] = @project_discovered_singleton_def_nodes
|
|
831
|
+
end
|
|
810
832
|
tables[:discovered_def_sources] = @project_discovered_def_sources unless @project_discovered_def_sources.empty?
|
|
811
833
|
unless @project_discovered_superclasses.empty?
|
|
812
834
|
tables[:discovered_superclasses] = @project_discovered_superclasses
|
|
@@ -816,7 +838,7 @@ module Rigor
|
|
|
816
838
|
tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
|
|
817
839
|
end
|
|
818
840
|
tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
|
|
819
|
-
tables
|
|
841
|
+
seed_member_layout_tables(tables)
|
|
820
842
|
# ADR-46 slice 1 — the class-declaration source map is read only by
|
|
821
843
|
# the ancestry accessors during dependency recording, so seed it
|
|
822
844
|
# only when recording is on; a normal run never carries it.
|
|
@@ -826,6 +848,16 @@ module Rigor
|
|
|
826
848
|
tables
|
|
827
849
|
end
|
|
828
850
|
|
|
851
|
+
# ADR-48 — seed the Data + Struct member-layout tables (each only when
|
|
852
|
+
# non-empty). Extracted to keep {#project_scope_seed_tables} under the
|
|
853
|
+
# complexity budget.
|
|
854
|
+
def seed_member_layout_tables(tables)
|
|
855
|
+
tables[:data_member_layouts] = @project_data_member_layouts unless @project_data_member_layouts.empty?
|
|
856
|
+
return if @project_struct_member_layouts.empty?
|
|
857
|
+
|
|
858
|
+
tables[:struct_member_layouts] = @project_struct_member_layouts
|
|
859
|
+
end
|
|
860
|
+
|
|
829
861
|
# ADR-46 slice 1 — when dependency recording is enabled, wrap the
|
|
830
862
|
# per-file analysis so the cross-file reads its inference makes are
|
|
831
863
|
# captured into `file_dependencies[path]`. Off by default: a normal
|
|
@@ -861,15 +893,8 @@ module Rigor
|
|
|
861
893
|
self_call_record = with_self_call_recording(path) do
|
|
862
894
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
863
895
|
end
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
root: parse_result.value,
|
|
867
|
-
scope_index: index,
|
|
868
|
-
self_call_misses: self_call_record ? self_call_record.calls : [],
|
|
869
|
-
comments: parse_result.comments,
|
|
870
|
-
disabled_rules: @configuration.disabled_rules
|
|
871
|
-
)
|
|
872
|
-
diagnostics += plugin_emitted_diagnostics(path, parse_result.value, scope)
|
|
896
|
+
self_call_misses = self_call_record ? self_call_record.calls : []
|
|
897
|
+
diagnostics = rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
|
|
873
898
|
diagnostics + explain_diagnostics(path, parse_result.value, scope)
|
|
874
899
|
rescue Errno::ENOENT => e
|
|
875
900
|
[
|
|
@@ -893,6 +918,28 @@ module Rigor
|
|
|
893
918
|
]
|
|
894
919
|
end
|
|
895
920
|
|
|
921
|
+
# ADR-53 B4 — the built-in node collectors and the plugin node rules
|
|
922
|
+
# share ONE traversal of the file. The collectors are built here (they
|
|
923
|
+
# need the completed `index`) and populated by the converged plugin
|
|
924
|
+
# walk; `node_results` carries the per-plugin node-rule output. Both
|
|
925
|
+
# the built-in `diagnose` output and the plugin diagnostics are then
|
|
926
|
+
# built from that single walk's results.
|
|
927
|
+
def rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
|
|
928
|
+
root = parse_result.value
|
|
929
|
+
node_collectors = CheckRules.build_node_collectors(path, index)
|
|
930
|
+
node_results = node_rule_results_by_plugin(path, root, scope, node_collectors, index)
|
|
931
|
+
diagnostics = CheckRules.diagnose(
|
|
932
|
+
path: path,
|
|
933
|
+
root: root,
|
|
934
|
+
scope_index: index,
|
|
935
|
+
self_call_misses: self_call_misses,
|
|
936
|
+
comments: parse_result.comments,
|
|
937
|
+
disabled_rules: @configuration.disabled_rules,
|
|
938
|
+
node_collectors: node_collectors
|
|
939
|
+
)
|
|
940
|
+
diagnostics + plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
941
|
+
end
|
|
942
|
+
|
|
896
943
|
# ADR-24 slice 4a — runs `block` (the typing pass) with the self-call
|
|
897
944
|
# recorder active when either the test-only `record_self_calls:` flag is
|
|
898
945
|
# set or the `call.self-undefined-method` rule resolves to a firing
|
|
@@ -928,7 +975,8 @@ module Rigor
|
|
|
928
975
|
else
|
|
929
976
|
Configuration::SeverityProfile.resolve(
|
|
930
977
|
rule: rule, authored_severity: :warning,
|
|
931
|
-
profile: @configuration.severity_profile, overrides: @configuration.severity_overrides
|
|
978
|
+
profile: @configuration.severity_profile, overrides: @configuration.severity_overrides,
|
|
979
|
+
bleeding_edge_overrides: @configuration.bleeding_edge_severity_overrides
|
|
932
980
|
) != :off
|
|
933
981
|
end
|
|
934
982
|
end
|
|
@@ -21,10 +21,9 @@ module Rigor
|
|
|
21
21
|
# reach the recorder. This module is the ADR-46 / ADR-47 "collect at
|
|
22
22
|
# evaluation time, never recompute" lesson applied to self-calls.
|
|
23
23
|
#
|
|
24
|
-
#
|
|
25
|
-
# closed-class gate
|
|
26
|
-
#
|
|
27
|
-
# plumbing OFF by default — {active?} is false on a normal run, so the
|
|
24
|
+
# ADR-24 slice 4 (`call.self-undefined-method`) consumes the recorded
|
|
25
|
+
# misses behind a confidently-closed-class gate (see `CheckRules` L775).
|
|
26
|
+
# The rule ships `:off` by default — {active?} is false on a normal run, so the
|
|
28
27
|
# instrumented choke-point pays a single integer read and records
|
|
29
28
|
# nothing. Recording is purely observational; it never changes a
|
|
30
29
|
# diagnostic.
|
|
@@ -21,21 +21,19 @@ module Rigor
|
|
|
21
21
|
module Analysis
|
|
22
22
|
# ADR-15 Phase 4a — per-worker analysis substrate.
|
|
23
23
|
# [ADR-15](../../../docs/adr/15-ractor-concurrency.md)
|
|
24
|
-
# § Phase 4 carves the
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# the absence of any Ractor coordination.
|
|
24
|
+
# § Phase 4 carves the Ractor-isolated worker pool into sub-phases;
|
|
25
|
+
# 4a/4b/4c all landed, but the Ractor pool (4b) is blocked by Ruby
|
|
26
|
+
# Bug #22075 (UAF) — the active pool backend is fork (ADR-15 Amendment).
|
|
27
|
+
# This class exists so the per-worker ownership boundary is testable
|
|
28
|
+
# independently of any pool coordinator.
|
|
30
29
|
#
|
|
31
30
|
# The constructor takes only `Ractor.shareable?` inputs:
|
|
32
31
|
#
|
|
33
32
|
# - `configuration` — Phase 2a ({Rigor::Configuration} is
|
|
34
33
|
# `Ractor.shareable?`).
|
|
35
|
-
# - `cache_store` —
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# for the no-Ractor coordinator path.
|
|
34
|
+
# - `cache_store` — the fork backend passes the parent runner's
|
|
35
|
+
# pre-built Store (`cache_store: @cache_store` in PoolCoordinator);
|
|
36
|
+
# workers share it rather than building their own at `cache_root`.
|
|
39
37
|
# - `plugin_blueprints` — Phase 3a
|
|
40
38
|
# (`Array<Plugin::Blueprint>` is `Ractor.shareable?`).
|
|
41
39
|
# - `explain` — Boolean.
|
|
@@ -175,14 +173,18 @@ module Rigor
|
|
|
175
173
|
|
|
176
174
|
scope = seed_project_scope(Scope.empty(environment: @environment, source_path: path))
|
|
177
175
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
176
|
+
# ADR-53 B4 — built-in collectors + plugin node rules share one walk.
|
|
177
|
+
node_collectors = CheckRules.build_node_collectors(path, index)
|
|
178
|
+
node_results = node_rule_results_by_plugin(path, parse_result.value, scope, node_collectors, index)
|
|
178
179
|
diagnostics = CheckRules.diagnose(
|
|
179
180
|
path: path,
|
|
180
181
|
root: parse_result.value,
|
|
181
182
|
scope_index: index,
|
|
182
183
|
comments: parse_result.comments,
|
|
183
|
-
disabled_rules: @configuration.disabled_rules
|
|
184
|
+
disabled_rules: @configuration.disabled_rules,
|
|
185
|
+
node_collectors: node_collectors
|
|
184
186
|
)
|
|
185
|
-
diagnostics += plugin_emitted_diagnostics(path, parse_result.value, scope)
|
|
187
|
+
diagnostics += plugin_emitted_diagnostics(path, parse_result.value, scope, node_results)
|
|
186
188
|
diagnostics + explain_diagnostics(path, parse_result.value, scope)
|
|
187
189
|
rescue Errno::ENOENT => e
|
|
188
190
|
[analyzer_error(path, e.message)]
|
|
@@ -230,11 +232,9 @@ module Rigor
|
|
|
230
232
|
Prism.parse(File.read(physical), filepath: path, version: @configuration.target_ruby)
|
|
231
233
|
end
|
|
232
234
|
|
|
233
|
-
# Mirrors {Runner#build_trust_policy}.
|
|
234
|
-
# 4b will need the same trust derivation, and the
|
|
235
|
-
# configuration is already shareable, so deriving it inside
|
|
235
|
+
# Mirrors {Runner#build_trust_policy}. Deriving trust inside
|
|
236
236
|
# the session keeps the substrate decoupled from the
|
|
237
|
-
# coordinator
|
|
237
|
+
# coordinator; configuration is already Ractor-shareable.
|
|
238
238
|
def build_trust_policy
|
|
239
239
|
trusted_gems = @configuration.plugins.map { |entry| trusted_gem_name(entry) }.uniq
|
|
240
240
|
roots = [Dir.pwd]
|
|
@@ -294,24 +294,30 @@ module Rigor
|
|
|
294
294
|
)
|
|
295
295
|
end
|
|
296
296
|
|
|
297
|
-
def plugin_emitted_diagnostics(path, root, scope)
|
|
297
|
+
def plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
298
298
|
return [] if @plugin_registry.empty?
|
|
299
299
|
|
|
300
|
-
# ADR-52 WD4 — single engine-owned node-rule walk per file; the
|
|
301
|
-
# results are bucketed per plugin (registry order) so emission
|
|
302
|
-
# stays plugin-major and byte-identical with the per-plugin walk.
|
|
303
|
-
node_results = node_rule_results_by_plugin(path, root, scope)
|
|
304
|
-
|
|
305
300
|
@plugin_registry.plugins.flat_map do |plugin|
|
|
306
301
|
collect_plugin_diagnostics(plugin, path, root, scope, node_results[plugin])
|
|
307
302
|
end
|
|
308
303
|
end
|
|
309
304
|
|
|
310
|
-
|
|
305
|
+
# ADR-52 WD4 + ADR-53 B4 — single engine-owned walk per file drives
|
|
306
|
+
# both the plugin node rules (bucketed per plugin in registry order,
|
|
307
|
+
# plugin-major emission) and the built-in node collectors
|
|
308
|
+
# (`node_collectors`, populated in place). Runs even with no node-rule
|
|
309
|
+
# plugins so the collectors still get driven (converged path).
|
|
310
|
+
def node_rule_results_by_plugin(path, root, scope, node_collectors, scope_index)
|
|
311
311
|
walk = @plugin_registry.node_rule_walk
|
|
312
|
-
|
|
312
|
+
driver = node_collectors && CheckRules.node_collector_driver(node_collectors)
|
|
313
|
+
return {}.compare_by_identity if walk.empty? && driver.nil?
|
|
313
314
|
|
|
314
|
-
results = walk.diagnostics_for_file(
|
|
315
|
+
results = walk.diagnostics_for_file(
|
|
316
|
+
path: path, scope: scope, root: root, collector_driver: driver
|
|
317
|
+
)
|
|
318
|
+
if ENV["RIGOR_SHADOW_RULE_WALK"]
|
|
319
|
+
CheckRules.shadow_verify_converged_collectors(path, root, scope_index, node_collectors)
|
|
320
|
+
end
|
|
315
321
|
results.each_with_object({}.compare_by_identity) do |result, by_plugin|
|
|
316
322
|
by_plugin[result.plugin] = result
|
|
317
323
|
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# ADR-50 § WD2 — the bleeding-edge overlay.
|
|
5
|
+
#
|
|
6
|
+
# A Rigor-maintained set of the *next major's* queued changes —
|
|
7
|
+
# severity-map promotions and new-discipline rule enablements — that a
|
|
8
|
+
# user can adopt early, before they become default-on at a major
|
|
9
|
+
# (ADR-50 § WD7). It is orthogonal to `severity_profile:` (how loud
|
|
10
|
+
# *today's* rules are) and is versioned with the gem, NOT a
|
|
11
|
+
# user-supplied file: the inspectable counterpart to PHPStan's
|
|
12
|
+
# `bleedingEdge` include.
|
|
13
|
+
#
|
|
14
|
+
# The overlay is **empty today** — no discipline has yet been queued
|
|
15
|
+
# for the next major. This module is the WD2 *foundation* (the v0.1.19
|
|
16
|
+
# slice): the surface (`bleeding_edge:` config, the
|
|
17
|
+
# `rigor show-bleedingedge` command, the severity-composition hook in
|
|
18
|
+
# {Configuration::SeverityProfile.resolve}) exists and is wired
|
|
19
|
+
# end-to-end, so the first real feature lands as a single {FEATURES}
|
|
20
|
+
# entry with no engine plumbing.
|
|
21
|
+
#
|
|
22
|
+
# Each feature carries a **stable feature id** — part of the ADR-50
|
|
23
|
+
# WD1 contract vocabulary: the config, the `show` command, and the
|
|
24
|
+
# eventual CHANGELOG migration note all name the same id, and a
|
|
25
|
+
# feature graduates to default-on at a major by being removed from
|
|
26
|
+
# {FEATURES}.
|
|
27
|
+
module BleedingEdge
|
|
28
|
+
# One queued change.
|
|
29
|
+
#
|
|
30
|
+
# @!attribute id
|
|
31
|
+
# @return [String] the stable feature id (contract vocabulary).
|
|
32
|
+
# @!attribute summary
|
|
33
|
+
# @return [String] a one-line description of what it changes.
|
|
34
|
+
# @!attribute severity_overrides
|
|
35
|
+
# @return [Hash{String => Symbol}] canonical rule id → the
|
|
36
|
+
# severity this feature imposes. Composed *below* the user's own
|
|
37
|
+
# `severity_overrides:` and *above* the active `severity_profile`
|
|
38
|
+
# (see {Configuration::SeverityProfile.resolve}).
|
|
39
|
+
Feature = Data.define(:id, :summary, :severity_overrides) do
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
"id" => id,
|
|
43
|
+
"summary" => summary,
|
|
44
|
+
"severity_overrides" => severity_overrides.transform_values(&:to_s)
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The overlay. Empty until the first next-major discipline is
|
|
50
|
+
# queued; add a {Feature} here (with a stable id) when one is.
|
|
51
|
+
FEATURES = [].freeze
|
|
52
|
+
|
|
53
|
+
module_function
|
|
54
|
+
|
|
55
|
+
# @return [Array<Feature>] the whole overlay.
|
|
56
|
+
def features
|
|
57
|
+
FEATURES
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Array<String>] every feature id in the overlay.
|
|
61
|
+
def feature_ids
|
|
62
|
+
FEATURES.map(&:id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param id [String]
|
|
66
|
+
# @return [Feature, nil]
|
|
67
|
+
def feature(id)
|
|
68
|
+
FEATURES.find { |f| f.id == id }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Resolves a normalized `bleeding_edge:` selector (see
|
|
72
|
+
# {Configuration#bleeding_edge}) to the active {Feature} list.
|
|
73
|
+
# Unknown ids in a `list` / `except` selector are simply absent from
|
|
74
|
+
# the overlay and contribute nothing — symmetric with how
|
|
75
|
+
# `severity_overrides:` keeps an unknown rule id inert until it
|
|
76
|
+
# lands (robust across gem versions).
|
|
77
|
+
#
|
|
78
|
+
# @param selector [Hash] `{ "mode" => "none" }`,
|
|
79
|
+
# `{ "mode" => "all" }`, `{ "mode" => "all", "except" => [ids] }`,
|
|
80
|
+
# or `{ "mode" => "list", "ids" => [ids] }`.
|
|
81
|
+
# @return [Array<Feature>]
|
|
82
|
+
def active_features(selector)
|
|
83
|
+
case selector["mode"]
|
|
84
|
+
when "all"
|
|
85
|
+
except = selector["except"] || []
|
|
86
|
+
FEATURES.reject { |f| except.include?(f.id) }
|
|
87
|
+
when "list"
|
|
88
|
+
ids = selector["ids"] || []
|
|
89
|
+
FEATURES.select { |f| ids.include?(f.id) }
|
|
90
|
+
else
|
|
91
|
+
[]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The merged severity-override map the active features impose for a
|
|
96
|
+
# selector. Frozen so the result is `Ractor.shareable?`.
|
|
97
|
+
#
|
|
98
|
+
# @param selector [Hash] see {#active_features}.
|
|
99
|
+
# @return [Hash{String => Symbol}]
|
|
100
|
+
def severity_overrides_for(selector)
|
|
101
|
+
active_features(selector).each_with_object({}) do |feature, acc|
|
|
102
|
+
acc.merge!(feature.severity_overrides)
|
|
103
|
+
end.freeze
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Feature ids named by a selector that are NOT in the overlay
|
|
107
|
+
# (typo / graduated / from a newer gem). Surfaced by
|
|
108
|
+
# `rigor show-bleedingedge` as a hint; never an error.
|
|
109
|
+
#
|
|
110
|
+
# @param selector [Hash] see {#active_features}.
|
|
111
|
+
# @return [Array<String>]
|
|
112
|
+
def unknown_selected_ids(selector)
|
|
113
|
+
named =
|
|
114
|
+
case selector["mode"]
|
|
115
|
+
when "list" then selector["ids"] || []
|
|
116
|
+
when "all" then selector["except"] || []
|
|
117
|
+
else []
|
|
118
|
+
end
|
|
119
|
+
known = feature_ids
|
|
120
|
+
named.reject { |id| known.include?(id) }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Builtins
|
|
7
|
+
# Refined types for predefined Ruby / stdlib constants whose upstream
|
|
8
|
+
# RBS signatures are broader than the constants' documented runtime
|
|
9
|
+
# invariants.
|
|
10
|
+
#
|
|
11
|
+
# Resolution is two-tiered:
|
|
12
|
+
#
|
|
13
|
+
# **Tier 1 — exact-value whitelist** (`FOLDED_CONSTANTS`):
|
|
14
|
+
# Constants whose value is bit-for-bit identical across every Ruby
|
|
15
|
+
# version and platform are folded to `Constant[T]`: the `Math::PI`
|
|
16
|
+
# / `Math::E` math constants (C's `M_PI` / `M_E`) and the four
|
|
17
|
+
# IEEE 754 binary64 magnitude constants `Float::INFINITY` /
|
|
18
|
+
# `::MAX` / `::MIN` / `::EPSILON` (each a single format-mandated bit
|
|
19
|
+
# pattern). Add new entries only when the value is truly
|
|
20
|
+
# cross-implementation invariant AND compares reflexively under
|
|
21
|
+
# `==` — the latter is why `Float::NAN` is deliberately EXCLUDED:
|
|
22
|
+
# `NaN == NaN` is `false`, so a `Constant[NAN]` would violate the
|
|
23
|
+
# `Type::Constant` `==` / `eql?` / `hash` contract (it would hash
|
|
24
|
+
# equal to itself yet compare unequal), corrupting type-equality
|
|
25
|
+
# and union dedup. The binary64 *integer* shape parameters
|
|
26
|
+
# (`Float::DIG` / `MANT_DIG` / `MAX_EXP` / …) are intentionally NOT
|
|
27
|
+
# folded: upstream RBS hedges them as "Usually defaults to …", and
|
|
28
|
+
# as plain `Integer`s they fall through Tier 2 to the RBS type
|
|
29
|
+
# harmlessly. `Complex::I` is deferred (no complex-fold consumer).
|
|
30
|
+
#
|
|
31
|
+
# **Tier 2 — runtime String inspection**:
|
|
32
|
+
# For any other constant, the module resolves it via `const_get`
|
|
33
|
+
# against the analyzer's own Ruby runtime. Core / stdlib constants
|
|
34
|
+
# (e.g. `RUBY_VERSION`, `RUBY_PLATFORM`) are always loaded into the
|
|
35
|
+
# analyzer process; project-defined constants are not (they live only
|
|
36
|
+
# in ASTs), so their `const_get` raises `NameError` and the lookup
|
|
37
|
+
# falls through to the RBS type tier.
|
|
38
|
+
#
|
|
39
|
+
# For a successfully resolved `String` value:
|
|
40
|
+
# - empty string → no refinement (fall through to RBS `String`)
|
|
41
|
+
# - a Ruby numeric literal → `numeric-string`
|
|
42
|
+
# - non-empty otherwise → `non-empty-string`
|
|
43
|
+
#
|
|
44
|
+
# **Exclusion set** (`RUNTIME_INSPECTION_EXCLUDED`):
|
|
45
|
+
# String constants that appear non-empty in the current runtime but
|
|
46
|
+
# are documented to be potentially empty in some build configuration
|
|
47
|
+
# or alternative implementation. Exclusions are populated by
|
|
48
|
+
# scanning Ruby's C source (version.c, etc.) and RBS comments for
|
|
49
|
+
# any constant whose documentation says "may be empty" or
|
|
50
|
+
# "platform-specific default". None are known today; the set
|
|
51
|
+
# exists as a safety net.
|
|
52
|
+
#
|
|
53
|
+
# This module is consulted by `Environment#constant_for_name` BEFORE
|
|
54
|
+
# the RBS constant-type table (widest types) but AFTER in-source
|
|
55
|
+
# constant writes (the user's own `Math::PI = 0.0` takes precedence
|
|
56
|
+
# via the lexical-candidate walk in `ExpressionTyper`).
|
|
57
|
+
module PredefinedConstantRefinements
|
|
58
|
+
# --- tier 1 -------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# Exact-value fold whitelist. Keys are unqualified constant paths
|
|
61
|
+
# (no leading "::") matching what `Environment#constant_for_name`
|
|
62
|
+
# receives.
|
|
63
|
+
FOLDED_CONSTANTS = {
|
|
64
|
+
# Math module — IEEE 754 bit-identical across all MRI / JRuby /
|
|
65
|
+
# TruffleRuby builds; folding enables precise constant arithmetic.
|
|
66
|
+
"Math::PI" => Type::Combinator.constant_of(::Math::PI).freeze,
|
|
67
|
+
"Math::E" => Type::Combinator.constant_of(::Math::E).freeze,
|
|
68
|
+
|
|
69
|
+
# Float magnitude limits — each a single format-mandated IEEE 754
|
|
70
|
+
# binary64 bit pattern (`+Inf`, `DBL_MAX`, `DBL_MIN`,
|
|
71
|
+
# `DBL_EPSILON`), reflexive under `==`. `Float::NAN` is excluded
|
|
72
|
+
# (non-reflexive `==` — see the module-level note).
|
|
73
|
+
"Float::INFINITY" => Type::Combinator.constant_of(::Float::INFINITY).freeze,
|
|
74
|
+
"Float::MAX" => Type::Combinator.constant_of(::Float::MAX).freeze,
|
|
75
|
+
"Float::MIN" => Type::Combinator.constant_of(::Float::MIN).freeze,
|
|
76
|
+
"Float::EPSILON" => Type::Combinator.constant_of(::Float::EPSILON).freeze
|
|
77
|
+
}.freeze
|
|
78
|
+
private_constant :FOLDED_CONSTANTS
|
|
79
|
+
|
|
80
|
+
# --- tier 2 -------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
# String constants whose runtime value is non-empty in the current
|
|
83
|
+
# Ruby but that should NOT be narrowed because they are documented
|
|
84
|
+
# to be potentially empty in some build or implementation.
|
|
85
|
+
#
|
|
86
|
+
# Methodology: grep Ruby's version.c and similar C sources, and the
|
|
87
|
+
# RBS comment corpus, for any constant annotated with "may be empty"
|
|
88
|
+
# or "platform-specific default". Add the full qualified path
|
|
89
|
+
# (without leading "::") when a genuine risk is found.
|
|
90
|
+
RUNTIME_INSPECTION_EXCLUDED = Set[].freeze
|
|
91
|
+
private_constant :RUNTIME_INSPECTION_EXCLUDED
|
|
92
|
+
|
|
93
|
+
NON_EMPTY_STRING = Type::Combinator.non_empty_string.freeze
|
|
94
|
+
NUMERIC_STRING = Type::Combinator.numeric_string.freeze
|
|
95
|
+
private_constant :NON_EMPTY_STRING, :NUMERIC_STRING
|
|
96
|
+
|
|
97
|
+
# --- public API ---------------------------------------------------
|
|
98
|
+
|
|
99
|
+
# @param name [String] unqualified constant name (e.g. `"Math::PI"`,
|
|
100
|
+
# `"RUBY_VERSION"`, `"Ruby::ENGINE"`)
|
|
101
|
+
# @return [Rigor::Type, nil] refined type, or nil to fall through
|
|
102
|
+
def self.lookup(name)
|
|
103
|
+
FOLDED_CONSTANTS[name] || inspect_runtime_string(name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# --- private ------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
# Resolves `name` via `const_get` in the analyzer's runtime and
|
|
109
|
+
# returns a refined String carrier, or nil.
|
|
110
|
+
def self.inspect_runtime_string(name)
|
|
111
|
+
return nil if RUNTIME_INSPECTION_EXCLUDED.include?(name)
|
|
112
|
+
|
|
113
|
+
mod = ::Object
|
|
114
|
+
name.split("::").each do |part|
|
|
115
|
+
# Resolve only constants already present — never let analysing a
|
|
116
|
+
# reference drive the analyzer's own runtime to autoload or run a
|
|
117
|
+
# `const_missing` hook. A `Digest::UUID` reference in project code
|
|
118
|
+
# otherwise makes `const_get` trigger `Digest.const_missing` →
|
|
119
|
+
# `require "digest/uuid"`, and a missing optional library raises
|
|
120
|
+
# `LoadError` (a `ScriptError`, not the `NameError` the const_get
|
|
121
|
+
# walk expects), which would abort the whole run rather than fall
|
|
122
|
+
# through to the RBS tier. `const_defined?(part, false)` answers
|
|
123
|
+
# the same "is this resolvable here" question without the side
|
|
124
|
+
# effect — a project-defined constant (the common case) is simply
|
|
125
|
+
# absent and returns nil, no exception raised.
|
|
126
|
+
return nil unless mod.is_a?(::Module) && mod.const_defined?(part, false)
|
|
127
|
+
|
|
128
|
+
mod = mod.const_get(part, false)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return nil unless mod.is_a?(::String) && !mod.empty?
|
|
132
|
+
|
|
133
|
+
classify_string(mod)
|
|
134
|
+
rescue ::NameError, ::TypeError, ::LoadError
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
private_class_method :inspect_runtime_string
|
|
138
|
+
|
|
139
|
+
# @param value [String] a non-empty string
|
|
140
|
+
# @return [Rigor::Type]
|
|
141
|
+
def self.classify_string(value)
|
|
142
|
+
if Type::Refined.ruby_numeric_literal?(value)
|
|
143
|
+
NUMERIC_STRING
|
|
144
|
+
else
|
|
145
|
+
NON_EMPTY_STRING
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
private_class_method :classify_string
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|