rigortype 0.2.9 → 0.3.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 +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -62,6 +62,17 @@ module Rigor
|
|
|
62
62
|
narrowing_facts methods: AssertionAnalyzer::SUPPORTED_METHODS do |call_node, scope|
|
|
63
63
|
AssertionAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
|
|
64
64
|
end
|
|
65
|
+
|
|
66
|
+
# ADR-88 WD1 — the narrowing facts this plugin contributes are derived purely from each assertion call's
|
|
67
|
+
# own AST at its call site (`assert_kind_of(String, x)` ⇒ `x` is `String` on the continuation); there is
|
|
68
|
+
# no cross-file catalog a cached diagnostic could depend on. A change to a file's assertions changes that
|
|
69
|
+
# file's own content (re-analysed by the incremental graph already). A stable sentinel declares "no
|
|
70
|
+
# cross-file fact surface", keeping the plugin incremental-capable (a contributing plugin with NO
|
|
71
|
+
# fact / producer / hook makes the incremental snapshot un-reusable every run); `--verify-incremental`
|
|
72
|
+
# backstops the claim.
|
|
73
|
+
def incremental_state_fingerprint
|
|
74
|
+
"per-file-assertions"
|
|
75
|
+
end
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
Rigor::Plugin.register(Minitest)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "policy_index"
|
|
@@ -69,7 +71,7 @@ module Rigor
|
|
|
69
71
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
70
72
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
71
73
|
else
|
|
72
|
-
node.
|
|
74
|
+
node.rigor_each_child { |child| walk_for_policies(child, lexical_path, &) }
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# have to list seven require lines.
|
|
3
|
+
# The Tier 1+2 Rails set, as a single require. Requiring this file registers every member class with
|
|
4
|
+
# `Rigor::Plugin`; the loader's lookup phase then finds them by id when `.rigor.yml` enumerates them.
|
|
6
5
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# UNWIRED — this entry point has no supported use today, and is kept deliberately (ADR-96 WD5) rather
|
|
7
|
+
# than removed. It was ADR-12 WD1's "Gemfile-convenience meta-gem": each plugin was to be its own gem, and
|
|
8
|
+
# `gem "rigor-rails"` was to pull the Rails set into a project's Gemfile in one line. Commit `9769f5fa`
|
|
9
|
+
# dropped the per-plugin gemspecs and ADR-31 settled distribution on the single bundled `rigortype` gem, so
|
|
10
|
+
# there is no `rigor-rails` gem to add to a Gemfile — and `plugins: [rigor-rails]` is rejected by the
|
|
11
|
+
# loader, which requires the members to be enumerated (or one selected with an explicit `id:`).
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# Adding the gem to a project's Gemfile without listing any plugin in `.rigor.yml` is harmless: the requires
|
|
16
|
-
# happen on `Bundler.require`, but no plugin's `init` / `prepare` / hooks run.
|
|
13
|
+
# ADR-96 WD3 proposes the role this file would fill: `plugins: [rigor-rails]` activating those members
|
|
14
|
+
# whose `target_gems:` are actually in the project's `Gemfile.lock`. If that is rejected, ADR-60 WD1's
|
|
15
|
+
# never-wired-surface criterion applies and this directory goes.
|
|
17
16
|
|
|
18
17
|
require "rigor-rails-routes"
|
|
19
18
|
require "rigor-rails-i18n"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -106,7 +108,7 @@ module Rigor
|
|
|
106
108
|
end
|
|
107
109
|
end
|
|
108
110
|
|
|
109
|
-
node.
|
|
111
|
+
node.rigor_each_child { |child| walk(child, prefix_path, declarations, superclasses) }
|
|
110
112
|
end
|
|
111
113
|
|
|
112
114
|
def record_class(node, qualified, declarations, superclasses)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -87,7 +89,7 @@ module Rigor
|
|
|
87
89
|
def visit_program_children(node)
|
|
88
90
|
return unless node.respond_to?(:compact_child_nodes)
|
|
89
91
|
|
|
90
|
-
node.
|
|
92
|
+
node.rigor_each_child do |child|
|
|
91
93
|
case child
|
|
92
94
|
when Prism::ModuleNode, Prism::ClassNode
|
|
93
95
|
visit_module_or_class(child)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -371,7 +373,7 @@ module Rigor
|
|
|
371
373
|
when Prism::CallNode
|
|
372
374
|
interpret_call(node, context)
|
|
373
375
|
else
|
|
374
|
-
node.
|
|
376
|
+
node.rigor_each_child { |child| interpret(child, context) }
|
|
375
377
|
end
|
|
376
378
|
end
|
|
377
379
|
|
|
@@ -526,7 +528,7 @@ module Rigor
|
|
|
526
528
|
return unless node.is_a?(Prism::Node)
|
|
527
529
|
|
|
528
530
|
yield node
|
|
529
|
-
node.
|
|
531
|
+
node.rigor_each_child { |child| walk_for_alias_pattern(child, &) }
|
|
530
532
|
end
|
|
531
533
|
|
|
532
534
|
def first_block_parameter_name(block_node)
|
|
@@ -545,7 +547,7 @@ module Rigor
|
|
|
545
547
|
body = node.block&.body
|
|
546
548
|
return if body.nil?
|
|
547
549
|
|
|
548
|
-
body.
|
|
550
|
+
body.rigor_each_child { |child| interpret(child, context) }
|
|
549
551
|
end
|
|
550
552
|
|
|
551
553
|
def handle_namespace(node, context)
|
|
@@ -625,7 +627,7 @@ module Rigor
|
|
|
625
627
|
return [] if body.nil?
|
|
626
628
|
|
|
627
629
|
skips = []
|
|
628
|
-
body.
|
|
630
|
+
body.rigor_each_child do |child|
|
|
629
631
|
next unless child.is_a?(Prism::CallNode) && child.name == :skip_controllers
|
|
630
632
|
next if child.receiver
|
|
631
633
|
|
|
@@ -748,7 +750,7 @@ module Rigor
|
|
|
748
750
|
body = context.concern_body(concern_name)
|
|
749
751
|
next if body.nil?
|
|
750
752
|
|
|
751
|
-
body.
|
|
753
|
+
body.rigor_each_child { |child| interpret(child, context) }
|
|
752
754
|
end
|
|
753
755
|
end
|
|
754
756
|
|
|
@@ -8,9 +8,10 @@ require "rigor/plugin"
|
|
|
8
8
|
# name: T`, `# @rbs return: T`, attribute `#:`, …) and contributes the result to the analysis environment
|
|
9
9
|
# through the `source_rbs_synthesizer:` manifest hook.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Since ADR-93 WD1 the default is `require_magic_comment: false`: a file is processed whenever it actually
|
|
12
|
+
# carries an annotation (see {Synthesizer#annotated?}), with only the upstream `# rbs_inline: disabled`
|
|
13
|
+
# directive opting a file out. Set `require_magic_comment: true` to restore the old ADR-32 WD2 gate that
|
|
14
|
+
# processed only files opening with `# rbs_inline: enabled`.
|
|
14
15
|
module Rigor
|
|
15
16
|
module Plugin
|
|
16
17
|
# The plugin gem requires `rbs/inline` at load time; without the upstream library the synthesizer can't
|
|
@@ -35,9 +36,24 @@ module Rigor
|
|
|
35
36
|
# env-build time. Returns the synthesised RBS source as a String, or `nil` when the file contributes
|
|
36
37
|
# nothing (no magic comment in the default mode, empty annotation set, parse error per WD6).
|
|
37
38
|
class Synthesizer
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
39
|
+
# An RDoc directive comment: `#:` followed by a bare word (RDoc allows a hyphen, as in `:call-seq:`)
|
|
40
|
+
# and a closing colon. RDoc predates rbs-inline by about two decades and `#:<name>:` is
|
|
41
|
+
# indistinguishable from an rbs-inline `#: <type>` assertion to upstream's parser, which reads the
|
|
42
|
+
# directive name as a type alias — `def f #:nodoc:` becomes `def f: () -> nodoc`, a type nothing
|
|
43
|
+
# declares (upstream issue: https://github.com/soutaro/rbs-inline/issues/248).
|
|
44
|
+
#
|
|
45
|
+
# Matching on shape rather than a name list covers all 17 directives the Ruby docs list
|
|
46
|
+
# (https://docs.ruby-lang.org/ja/latest/library/rdoc.html) with nothing to maintain, and none of
|
|
47
|
+
# `#: () -> void`, `#: String`, `#:Integer`, `#: bool`, `#:my_alias`, `#: { name: String }`,
|
|
48
|
+
# `#: (name: String) -> void`, `#: Integer?`, `#[Integer]` or `# @rbs x: String` matches it: a valid
|
|
49
|
+
# RBS type never starts with a bare lowercase word that is immediately closed by a colon. Only the
|
|
50
|
+
# space-free spelling is affected; `# :nodoc:` never reaches upstream's annotation grammar.
|
|
51
|
+
RDOC_DIRECTIVE_COMMENT = /\A#:[a-z_][\w-]*:/
|
|
52
|
+
|
|
53
|
+
# @param require_magic_comment [Boolean] when `false` (the default since ADR-93 WD1), the magic
|
|
54
|
+
# comment is not required and the file is processed only if it actually carries an annotation — see
|
|
55
|
+
# {#annotated?}. When `true` (the old ADR-32 WD2 gate), only files opening with
|
|
56
|
+
# `# rbs_inline: enabled` are processed, and upstream's opt-in semantics apply verbatim.
|
|
41
57
|
def initialize(require_magic_comment:)
|
|
42
58
|
@require_magic_comment = require_magic_comment
|
|
43
59
|
freeze
|
|
@@ -55,6 +71,9 @@ module Rigor
|
|
|
55
71
|
return nil if source.empty?
|
|
56
72
|
|
|
57
73
|
result = ::Prism.parse(source)
|
|
74
|
+
_, result = neutralize_rdoc_directives(source, result)
|
|
75
|
+
return nil if !@require_magic_comment && !annotated?(result)
|
|
76
|
+
|
|
58
77
|
# `opt_in: true` is rbs-inline's "require the magic comment" mode (per upstream parser.rb:62).
|
|
59
78
|
# The plugin's `require_magic_comment:` config knob maps directly onto it.
|
|
60
79
|
parsed = ::RBS::Inline::Parser.parse(result, opt_in: @require_magic_comment)
|
|
@@ -71,6 +90,56 @@ module Rigor
|
|
|
71
90
|
# message, without crashing analysis.
|
|
72
91
|
[:error, "#{e.class}: #{e.message.to_s.lines.first.to_s.strip}"]
|
|
73
92
|
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# True when the file carries at least one rbs-inline annotation. Gates the magic-comment-free mode,
|
|
97
|
+
# and is the difference between "honour annotations wherever they are" and "fabricate signatures for
|
|
98
|
+
# code nobody annotated" — upstream's opt-out mode does the latter, emitting a full
|
|
99
|
+
# `def f: (untyped x) -> untyped` skeleton for EVERY unannotated def. Rigor trusts an accepted
|
|
100
|
+
# signature over body inference, so those skeletons would replace real inferred types with `untyped`:
|
|
101
|
+
# measured on mail (zero annotations) as 26 → 42 diagnostics, i.e. the mode actively fought the
|
|
102
|
+
# analysis on exactly the projects that write no annotations. The spec binds Rigor to treat
|
|
103
|
+
# annotations as type sources "whenever present" (`overview.md` § "Compatibility hierarchy"); it does
|
|
104
|
+
# not ask for untyped shadows of unannotated code.
|
|
105
|
+
#
|
|
106
|
+
# Detection delegates to upstream's own `AnnotationParser` rather than scanning for `#:` / `@rbs`
|
|
107
|
+
# with a regexp: the annotation grammar is upstream's to define (ADR-32 WD3), and this keeps a doc
|
|
108
|
+
# comment that merely mentions `@rbs`, or a URL containing `#:`, from opting a file in. A file with
|
|
109
|
+
# the magic comment keeps upstream's semantics verbatim — the author opted that file in explicitly,
|
|
110
|
+
# skeletons and all, which is what `rbs-inline --output` would generate for it.
|
|
111
|
+
def annotated?(prism_result)
|
|
112
|
+
::RBS::Inline::AnnotationParser.parse(prism_result.comments)
|
|
113
|
+
.any? { |parsed| parsed.each_annotation.any? }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Rewrite every RDoc directive comment to its spaced spelling (`#:nodoc:` -> `# :nodoc:`) so
|
|
117
|
+
# upstream's annotation grammar never sees it, and re-parse. Two reasons this happens here rather
|
|
118
|
+
# than in {#annotated?}: the directive must not gate a file in, AND it must not reach the synthesis,
|
|
119
|
+
# because one mis-parsed directive takes the whole class down with it. `def f #:nodoc:` renders as
|
|
120
|
+
# `def f: (untyped x) -> nodoc`; `nodoc` resolves to nothing, `RBS::DefinitionBuilder` raises
|
|
121
|
+
# `NoTypeFoundError` for the class, and every real annotation in that class is silently lost —
|
|
122
|
+
# measured on a class whose `#: (String) -> Integer` method fell back to body inference because a
|
|
123
|
+
# sibling method carried `#:nodoc:`. `fileutils.rb` alone carries 29 of these.
|
|
124
|
+
#
|
|
125
|
+
# The rewrite touches comment text only, so it cannot change what Ruby does; it shifts columns
|
|
126
|
+
# within the comment, which nothing downstream reads (the synthesized RBS is fresh text with its own
|
|
127
|
+
# buffer). Prism decides what is a comment, so a `#:nodoc:` inside a string literal stays put. Files
|
|
128
|
+
# without a directive re-parse nothing.
|
|
129
|
+
def neutralize_rdoc_directives(source, prism_result)
|
|
130
|
+
offsets = prism_result.comments.filter_map do |comment|
|
|
131
|
+
location = comment.location
|
|
132
|
+
# `start_character_offset`, not `start_offset`: the latter counts BYTES while `String#insert`
|
|
133
|
+
# indexes CHARACTERS, so on a file with any multi-byte content the space lands mid-word and
|
|
134
|
+
# rewrites `#:nodoc:` to `#:n odoc:` — still a directive to upstream, and now a corrupted one.
|
|
135
|
+
location.start_character_offset if RDOC_DIRECTIVE_COMMENT.match?(location.slice)
|
|
136
|
+
end
|
|
137
|
+
return [source, prism_result] if offsets.empty?
|
|
138
|
+
|
|
139
|
+
# Back to front, so each insertion leaves the earlier offsets valid.
|
|
140
|
+
rewritten = offsets.sort.reverse.inject(source.dup) { |acc, offset| acc.insert(offset + 1, " ") }
|
|
141
|
+
[rewritten, ::Prism.parse(rewritten)]
|
|
142
|
+
end
|
|
74
143
|
end
|
|
75
144
|
|
|
76
145
|
manifest(
|
|
@@ -87,11 +156,16 @@ module Rigor
|
|
|
87
156
|
# override (returned by `#manifest`, which `Plugin::Registry#source_rbs_synthesizers` consults via
|
|
88
157
|
# `plugin.manifest.source_rbs_synthesizer`).
|
|
89
158
|
#
|
|
90
|
-
# ADR-
|
|
91
|
-
#
|
|
159
|
+
# ADR-93 WD1 — `require_magic_comment` defaults to `false`: annotations are official type sources
|
|
160
|
+
# "always parsed whenever present" per the binding spec (overview.md § Compatibility hierarchy), so
|
|
161
|
+
# a file is processed when it actually carries an annotation, with the magic comment not required.
|
|
162
|
+
# The magic-comment-free mode is annotation-presence-gated (see {Synthesizer#annotated?}), so an
|
|
163
|
+
# unannotated file contributes nothing and the flip cannot regress it. Set `require_magic_comment:
|
|
164
|
+
# true` in `.rigor.yml` to restore the ADR-32 opt-in behaviour; `# rbs_inline: disabled` remains the
|
|
165
|
+
# per-file opt-out either way (honoured by upstream unconditionally).
|
|
92
166
|
def initialize(services:, config: {})
|
|
93
167
|
super
|
|
94
|
-
@require_magic_comment = config.fetch("require_magic_comment",
|
|
168
|
+
@require_magic_comment = config.fetch("require_magic_comment", false) ? true : false
|
|
95
169
|
@synthesizer = Synthesizer.new(require_magic_comment: @require_magic_comment)
|
|
96
170
|
# Build the per-instance manifest eagerly (before `freeze`) so the registry's repeated reads
|
|
97
171
|
# return the same object and we don't need to mutate a frozen instance later.
|
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
# rigor-rbs-inline — ingests rbs-inline-shaped comments (`# @rbs name: T`, `#: () -> T`, `# @rbs return: T`,
|
|
4
4
|
# attribute `#:`, ivars, generics, override, …) as RBS contributions to the analysis environment.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Since ADR-93 WD1 the plugin defaults to `require_magic_comment: false`: annotations are official type
|
|
7
|
+
# sources wherever present, gated only on a file actually carrying one, with `# rbs_inline: disabled` the
|
|
8
|
+
# per-file opt-out. ADR-93 WD2 default-wires this plugin from `Configuration.load` when the upstream
|
|
9
|
+
# `rbs-inline` library is resolvable, so most projects need no `plugins:` entry at all. Restore the old
|
|
10
|
+
# ADR-32 magic-comment gate, or opt out of the auto-wire, through config:
|
|
10
11
|
#
|
|
11
12
|
# # .rigor.yml
|
|
12
13
|
# plugins:
|
|
13
14
|
# - id: rigor-rbs-inline
|
|
14
15
|
# config:
|
|
15
|
-
# require_magic_comment:
|
|
16
|
+
# require_magic_comment: true # default false — restore the ADR-32 magic-comment gate
|
|
17
|
+
# # or disable the auto-wired default entirely:
|
|
18
|
+
# # - gem: rigor-rbs-inline
|
|
19
|
+
# # enabled: false
|
|
16
20
|
#
|
|
17
21
|
# The plugin depends on the upstream `rbs-inline` gem; Rigor's core `rigortype` gemspec stays zero-dep per
|
|
18
22
|
# ADR-0 / ADR-32 WD1.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -89,7 +91,7 @@ module Rigor
|
|
|
89
91
|
return
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
node.
|
|
94
|
+
node.rigor_each_child do |child|
|
|
93
95
|
collect(child, anchor: anchor, accumulator: accumulator)
|
|
94
96
|
end
|
|
95
97
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -70,7 +72,7 @@ module Rigor
|
|
|
70
72
|
if rspec_describe_call?(node)
|
|
71
73
|
scopes << build_scope(node, kind: :rspec_describe)
|
|
72
74
|
else
|
|
73
|
-
node.
|
|
75
|
+
node.rigor_each_child { |child| walk_top_level(child, scopes) }
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
|
|
@@ -97,6 +97,17 @@ module Rigor
|
|
|
97
97
|
let_binding_return_type(call_node, scope)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# ADR-88 WD1 — both contributions are per-file: the matcher narrowing facts come from the call's own AST,
|
|
101
|
+
# and the `let`-binding return types come from each spec file's OWN `describe`/`let` structure (`let` is
|
|
102
|
+
# file-scoped). A spec file's edit changes its own content (re-analysed already). The one cross-file input
|
|
103
|
+
# — the optional factorybot `:factory_index` fact this plugin consumes — is itself an ADR-9 publication
|
|
104
|
+
# captured in the incremental fact-surface digest via factorybot's own channel, so it need not appear
|
|
105
|
+
# here. A stable sentinel declares "no OWN cross-file fact surface", keeping the plugin
|
|
106
|
+
# incremental-capable; `--verify-incremental` backstops the claim.
|
|
107
|
+
def incremental_state_fingerprint
|
|
108
|
+
"per-file-lets"
|
|
109
|
+
end
|
|
110
|
+
|
|
100
111
|
private
|
|
101
112
|
|
|
102
113
|
# The `file_methods:` gate set — every `let` / `subject` name the file declares anywhere. A safe
|
|
@@ -43,7 +43,10 @@ module Rigor
|
|
|
43
43
|
|
|
44
44
|
require "rack/utils"
|
|
45
45
|
@rack_status_symbols = Rack::Utils::SYMBOL_TO_STATUS_CODE.keys.to_set.freeze
|
|
46
|
-
rescue LoadError, StandardError
|
|
46
|
+
rescue ::LoadError, ::StandardError
|
|
47
|
+
# `::`-qualified: in this lexical scope a bare `LoadError` resolves to `Rigor::Plugin::LoadError`
|
|
48
|
+
# (the plugin-loading error, a StandardError), so the real `::LoadError` (a ScriptError) from a
|
|
49
|
+
# failed require would escape instead of declining.
|
|
47
50
|
@rack_status_symbols = nil
|
|
48
51
|
end
|
|
49
52
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "worker_index"
|
|
@@ -67,7 +69,7 @@ module Rigor
|
|
|
67
69
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
68
70
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
69
71
|
else
|
|
70
|
-
node.
|
|
72
|
+
node.rigor_each_child { |child| walk_for_workers(child, lexical_path, &) }
|
|
71
73
|
end
|
|
72
74
|
end
|
|
73
75
|
|
|
@@ -133,7 +135,7 @@ module Rigor
|
|
|
133
135
|
def lookup_perform_def(body)
|
|
134
136
|
return nil if body.nil?
|
|
135
137
|
|
|
136
|
-
body.
|
|
138
|
+
body.rigor_each_child do |node|
|
|
137
139
|
next unless node.is_a?(Prism::DefNode) && node.name == :perform
|
|
138
140
|
next if node.receiver.is_a?(Prism::SelfNode)
|
|
139
141
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "method_signature"
|
|
@@ -58,7 +60,7 @@ module Rigor
|
|
|
58
60
|
# A `def` not preceded by a `sig` is fine; we just don't record anything for it. The
|
|
59
61
|
# interesting case is in `walk_statements`, which pairs sig+def.
|
|
60
62
|
else
|
|
61
|
-
node.
|
|
63
|
+
node.rigor_each_child do |child|
|
|
62
64
|
walk_node(child, state, lexical_path: lexical_path, in_singleton_class: in_singleton_class)
|
|
63
65
|
end
|
|
64
66
|
end
|
|
@@ -116,6 +116,18 @@ module Rigor
|
|
|
116
116
|
errors.map { |error| parse_error_diagnostic(path, error) }
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# ADR-88 WD2 — the per-run catalog is an ADR-60 record-and-validate producer. Before this it was rebuilt
|
|
120
|
+
# unconditionally on every `--incremental` recheck (parse + walk every `.rb` / `.rbi` sig tree); as a
|
|
121
|
+
# producer it is cached to disk keyed on the plugin config, with `watch:` covering the scanned trees so an
|
|
122
|
+
# added / removed / edited sig file recomputes it. The producer VALUE (a Marshal-clean bundle of the
|
|
123
|
+
# catalog + the sigil map + the parse-error tuples) is what ADR-88 WD1 digests into the incremental
|
|
124
|
+
# fact-surface fingerprint, so a sig edit OUTSIDE `signature_paths:` invalidates the snapshot for free.
|
|
125
|
+
# The bundle is returned (not written to ivars) because a cache HIT does not run the block — the sigil map
|
|
126
|
+
# and parse errors would otherwise be lost on the warm path.
|
|
127
|
+
producer :catalog, watch: -> { catalog_watch_globs } do |_params|
|
|
128
|
+
build_catalog_bundle
|
|
129
|
+
end
|
|
130
|
+
|
|
119
131
|
# ADR-52 slice 4 — per-call return-type path via the method-name-gated `dynamic_return` DSL. The
|
|
120
132
|
# recognised name set is only known at run time (the catalog's `def` names come from the lazy
|
|
121
133
|
# catalog build), so it is declared as a callable: the engine `instance_exec`s it once per run on
|
|
@@ -389,35 +401,69 @@ module Rigor
|
|
|
389
401
|
end
|
|
390
402
|
end
|
|
391
403
|
|
|
404
|
+
# ADR-88 WD2 — resolve the catalog from the `:catalog` producer (disk-cached) and unpack its Marshal-clean
|
|
405
|
+
# bundle into the ivars the return-type / sigil-gate / parse-error paths read. `producer_value` memoises
|
|
406
|
+
# per instance and rescues a producer failure to nil, hence the empty-bundle fallback.
|
|
392
407
|
def ensure_catalog
|
|
393
408
|
return @catalog if @catalog_built
|
|
394
409
|
|
|
410
|
+
bundle = producer_value(:catalog) || EMPTY_CATALOG_BUNDLE
|
|
411
|
+
@catalog = bundle[:catalog]
|
|
412
|
+
@sigil_by_path = bundle[:sigil_by_path]
|
|
413
|
+
@parse_errors_by_path = bundle[:parse_errors_by_path]
|
|
414
|
+
@catalog_built = true
|
|
415
|
+
@catalog
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# ADR-88 WD2 — the `watch:` coverage for the `:catalog` producer: the `.rb` sig trees + the `.rbi` trees.
|
|
419
|
+
# An added / removed file under either invalidates the cached bundle even though the block globbed the
|
|
420
|
+
# tree itself. Evaluated at `cache_for` time (after `#init`), so `@configured_paths` / `@rbi_paths` are set.
|
|
421
|
+
def catalog_watch_globs
|
|
422
|
+
[[@configured_paths, "**/*.rb"], [@rbi_paths, "**/*.rbi"]]
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# ADR-88 WD2 — build the Marshal-clean catalog bundle the `:catalog` producer caches. Harvests the `.rb`
|
|
426
|
+
# sig trees then the `.rbi` trees (last-wins ordering preserved from the ivar-mutating original) and
|
|
427
|
+
# returns `{ catalog:, sigil_by_path:, parse_errors_by_path: }`.
|
|
428
|
+
def build_catalog_bundle
|
|
395
429
|
catalog = Catalog.new
|
|
430
|
+
sigil_by_path = {}
|
|
431
|
+
parse_errors_by_path = {}
|
|
396
432
|
# Project source — `.rb` only.
|
|
397
|
-
@configured_paths.each { |root| harvest_path(root, catalog,
|
|
433
|
+
@configured_paths.each { |root| harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, %w[.rb]) }
|
|
398
434
|
# Sorbet RBI tree — `.rbi` only. Slice 4 of ADR-11.
|
|
399
|
-
@rbi_paths.each { |root| harvest_path(root, catalog,
|
|
435
|
+
@rbi_paths.each { |root| harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, %w[.rbi]) }
|
|
400
436
|
catalog.freeze!
|
|
401
|
-
|
|
402
|
-
@catalog_built = true
|
|
403
|
-
catalog
|
|
437
|
+
{ catalog: catalog, sigil_by_path: sigil_by_path, parse_errors_by_path: parse_errors_by_path }
|
|
404
438
|
end
|
|
405
439
|
|
|
440
|
+
# Frozen empty bundle used when the `:catalog` producer failed (e.g. a project I/O error) so downstream
|
|
441
|
+
# reads see a well-formed shape rather than nil.
|
|
442
|
+
EMPTY_CATALOG_BUNDLE = { catalog: Catalog.new.freeze!, sigil_by_path: {}, parse_errors_by_path: {} }.freeze
|
|
443
|
+
private_constant :EMPTY_CATALOG_BUNDLE
|
|
444
|
+
|
|
406
445
|
# @param root [String] directory or single file.
|
|
407
446
|
# @param catalog [Catalog]
|
|
447
|
+
# @param sigil_by_path [Hash{String=>Symbol}] accumulator: harvested file → detected sigil level.
|
|
448
|
+
# @param parse_errors_by_path [Hash{String=>Array<Hash>}] accumulator: file → `{kind:,line:,column:}` tuples.
|
|
408
449
|
# @param extensions [Array<String>] file extensions to accept (e.g. `[".rb"]` for project source,
|
|
409
450
|
# `[".rbi"]` for Sorbet RBI tree).
|
|
410
|
-
def harvest_path(root, catalog, extensions
|
|
451
|
+
def harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, extensions)
|
|
411
452
|
absolute = canonicalize(root)
|
|
412
453
|
if File.directory?(absolute)
|
|
413
454
|
extensions.each do |ext|
|
|
455
|
+
# ADR-88 WD2 — deterministic fold order matters now that the catalog VALUE is digested into the
|
|
456
|
+
# incremental fact-surface fingerprint: a duplicate `(class, method, kind)` sig's last-wins winner
|
|
457
|
+
# must not vary by machine / run. `Dir.glob` sorts its results by default on Ruby 3.0+, so the fold
|
|
458
|
+
# order is already stable (the slice-1 comment's "filesystem order" caveat predates that default);
|
|
459
|
+
# this walk relies on it rather than re-sorting.
|
|
414
460
|
Dir.glob(File.join(absolute, "**", "*#{ext}")).each do |path|
|
|
415
|
-
harvest_file(canonicalize(path), catalog)
|
|
461
|
+
harvest_file(canonicalize(path), catalog, sigil_by_path, parse_errors_by_path)
|
|
416
462
|
end
|
|
417
463
|
end
|
|
418
464
|
elsif File.file?(absolute) && extensions.any? { |ext| absolute.end_with?(ext) }
|
|
419
465
|
# `paths:` may list individual files (the demos do this); walk them directly rather than skipping.
|
|
420
|
-
harvest_file(absolute, catalog)
|
|
466
|
+
harvest_file(absolute, catalog, sigil_by_path, parse_errors_by_path)
|
|
421
467
|
end
|
|
422
468
|
end
|
|
423
469
|
|
|
@@ -432,7 +478,7 @@ module Rigor
|
|
|
432
478
|
expanded
|
|
433
479
|
end
|
|
434
480
|
|
|
435
|
-
def harvest_file(path, catalog)
|
|
481
|
+
def harvest_file(path, catalog, sigil_by_path, parse_errors_by_path)
|
|
436
482
|
contents = io_boundary.read_file(path)
|
|
437
483
|
return if contents.nil?
|
|
438
484
|
|
|
@@ -441,7 +487,7 @@ module Rigor
|
|
|
441
487
|
# Per-call-site assertion gating consults this map at recognition time. Recorded BEFORE the
|
|
442
488
|
# ignored short-circuit so a `# typed: ignore` file still reports its level to the gate (the gate
|
|
443
489
|
# then chooses to suppress assertions there too — `ignore` is stricter than `false`).
|
|
444
|
-
|
|
490
|
+
sigil_by_path[path] = level
|
|
445
491
|
return if SigilDetector.ignored?(level)
|
|
446
492
|
|
|
447
493
|
result = Prism.parse(contents)
|
|
@@ -460,13 +506,23 @@ module Rigor
|
|
|
460
506
|
end
|
|
461
507
|
|
|
462
508
|
errors = CatalogWalker.walk(root: result.value, catalog: sig_catalog, path: path)
|
|
463
|
-
|
|
509
|
+
# ADR-88 WD2 — store Marshal-clean `{kind:, line:, column:}` tuples, not the `ParseError` (it holds a
|
|
510
|
+
# live Prism node the producer bundle could not serialise). `diagnostics_for_file` rebuilds the
|
|
511
|
+
# diagnostic from the tuple, at the same 1-based line / `start_column + 1` position `from_node` gave.
|
|
512
|
+
parse_errors_by_path[path] = errors.map { |error| parse_error_tuple(error) } unless errors.empty?
|
|
464
513
|
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
465
514
|
# Skip files outside the trusted read scope or that vanished between glob and read; the plugin
|
|
466
515
|
# produces no output for them.
|
|
467
516
|
nil
|
|
468
517
|
end
|
|
469
518
|
|
|
519
|
+
# ADR-88 WD2 — the Marshal-clean position tuple for a `CatalogWalker::ParseError`, capturing the node's
|
|
520
|
+
# location at harvest time so the producer bundle carries no live Prism node.
|
|
521
|
+
def parse_error_tuple(error)
|
|
522
|
+
location = error.node.location
|
|
523
|
+
{ kind: error.kind, line: location.start_line, column: location.start_column + 1 }
|
|
524
|
+
end
|
|
525
|
+
|
|
470
526
|
# Emits a `plugin.sorbet.absurd-reachable` warning for the `T.absurd(x)` call recorded in
|
|
471
527
|
# `@reachable_absurd_nodes` during inference; the node rule above does the identity match and pop.
|
|
472
528
|
def absurd_diagnostic(path, call_node)
|
|
@@ -535,11 +591,15 @@ module Rigor
|
|
|
535
591
|
)
|
|
536
592
|
end
|
|
537
593
|
|
|
594
|
+
# ADR-88 WD2 — `error` is a Marshal-clean `{kind:, line:, column:}` tuple (the pre-computed node
|
|
595
|
+
# location), not a Prism-node-bearing `ParseError`; build the diagnostic directly at that position (the
|
|
596
|
+
# `line` / `column` `from_node` would have derived).
|
|
538
597
|
def parse_error_diagnostic(path, error)
|
|
539
|
-
Rigor::Analysis::Diagnostic.
|
|
540
|
-
error.node,
|
|
598
|
+
Rigor::Analysis::Diagnostic.new(
|
|
541
599
|
path: path,
|
|
542
|
-
|
|
600
|
+
line: error[:line],
|
|
601
|
+
column: error[:column],
|
|
602
|
+
message: parse_error_message(error[:kind]),
|
|
543
603
|
severity: :warning,
|
|
544
604
|
rule: "parse-error"
|
|
545
605
|
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Declares the reopening `Rigor::Source::NodeChildren` performs at load: a compiled `#rigor_each_child` is defined on
|
|
2
|
+
# every concrete `Prism::*Node` class (allocation-free equivalent of `compact_child_nodes.each`). Declared on the
|
|
3
|
+
# abstract base so every subclass resolves; the runtime definition is per-concrete-class, generated from
|
|
4
|
+
# `Prism::Reflection` in lib/rigor/source/node_children.rb.
|
|
5
|
+
module Prism
|
|
6
|
+
class Node
|
|
7
|
+
def rigor_each_child: () { (Prism::Node) -> void } -> void
|
|
8
|
+
end
|
|
9
|
+
end
|
data/sig/rigor/cache.rbs
CHANGED
|
@@ -14,6 +14,12 @@ module Rigor
|
|
|
14
14
|
# PRODUCER_ID constant and a private `self.compute(loader)`.
|
|
15
15
|
class RbsCacheProducer
|
|
16
16
|
def self.fetch: (loader: untyped, store: untyped) -> untyped
|
|
17
|
+
|
|
18
|
+
# The generation cap this producer declares to `Cache::Store#evict!`
|
|
19
|
+
# (issue #151). `rigor sig-gen` infers the literal `2` from the base
|
|
20
|
+
# body; the contract is the wider `Integer`, since a subclass may
|
|
21
|
+
# override it.
|
|
22
|
+
def self.generation_cap: () -> Integer
|
|
17
23
|
end
|
|
18
24
|
end
|
|
19
25
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Rigor
|
|
2
|
+
module Inference
|
|
3
|
+
# `VoidOrigin = Data.define(:class_name, :method_name, :kind)`. Declared as a `::Data` subclass because
|
|
4
|
+
# that is the shape the runtime actually builds; `sig-gen` cannot yet read a `Data.define` assignment
|
|
5
|
+
# (it reports the block body's `#label` against the enclosing namespace and drops the members).
|
|
6
|
+
class VoidOrigin < ::Data
|
|
7
|
+
attr_reader class_name: String
|
|
8
|
+
attr_reader method_name: Symbol
|
|
9
|
+
attr_reader kind: Symbol
|
|
10
|
+
|
|
11
|
+
def self.new: (class_name: String, method_name: Symbol, kind: Symbol) -> instance
|
|
12
|
+
| (String class_name, Symbol method_name, Symbol kind) -> instance
|
|
13
|
+
|
|
14
|
+
# A human-facing `Class#method` / `Class.method` label for the diagnostic message.
|
|
15
|
+
def label: () -> String
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|