rigortype 0.2.9 → 0.3.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 +2 -2
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +1 -0
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- 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 +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- 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 +68 -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 +343 -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_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +33 -3
- 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 +212 -27
- 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 +163 -23
- 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 +1 -32
- 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 +35 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- 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 +11 -0
- 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/completion_provider.rb +2 -1
- 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/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- 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 +24 -2
- 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-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/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- 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 +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -63,12 +63,28 @@ module Rigor
|
|
|
63
63
|
produces: [:dry_type_aliases]
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
66
|
+
# Cached alias-table producer (ADR-60 WD3 record-and-validate). The scan Prism-parses every `.rb` file
|
|
67
|
+
# under the project's `paths:` tree to find `include Dry.Types()` declarations — an expensive pass that
|
|
68
|
+
# ran on EVERY invocation (cold and warm) because `#prepare` called {AliasScanner.scan} directly, with
|
|
69
|
+
# no cache. It now rides `cache_for`: a warm run re-globs + re-digests the watched tree (cheap SHA over
|
|
70
|
+
# file bytes, no AST build) and reuses the cached table instead of re-parsing. This is the dominant win
|
|
71
|
+
# even for a project that ships no dry-types module — the parse still has to run to prove that, and now
|
|
72
|
+
# the empty result is cached too.
|
|
73
|
+
#
|
|
74
|
+
# `watch:` covers exactly the files the scan reads: one glob per project `paths:` entry, co-extensive
|
|
75
|
+
# with {#scannable_paths}. A {Cache::Descriptor::GlobEntry} digests every file matching its glob, so a
|
|
76
|
+
# content edit, a file addition, and a file removal anywhere under those paths all move the digest and
|
|
77
|
+
# invalidate the entry. The scan reads no file individually through the `IoBoundary`, so the evaluated
|
|
78
|
+
# `watch:` globs are the entire dependency descriptor — and they fully cover the scan's input.
|
|
79
|
+
producer :dry_type_aliases, watch: -> { alias_watch_globs } do |_params|
|
|
80
|
+
AliasScanner.scan(paths: scannable_paths)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Builds the (cached) alias table and publishes it via the ADR-9 fact store. `producer_value` runs the
|
|
84
|
+
# producer through `cache_for` and memoises the result — including a rescued nil — for the run.
|
|
69
85
|
def prepare(services)
|
|
70
|
-
aliases =
|
|
71
|
-
return if aliases.empty?
|
|
86
|
+
aliases = producer_value(:dry_type_aliases)
|
|
87
|
+
return if aliases.nil? || aliases.empty?
|
|
72
88
|
|
|
73
89
|
services.fact_store.publish(
|
|
74
90
|
plugin_id: manifest.id,
|
|
@@ -86,7 +102,7 @@ module Rigor
|
|
|
86
102
|
# Resolves the project's `paths:` to a flat list of `.rb` files the scanner walks. Mirrors
|
|
87
103
|
# `Analysis::Runner`'s `expand_paths` floor; we don't need the runner's full exclude/sort surface
|
|
88
104
|
# because the alias table is a union — any duplicate scan is a no-op.
|
|
89
|
-
def scannable_paths
|
|
105
|
+
def scannable_paths
|
|
90
106
|
@scannable_paths ||= services.configuration.paths.flat_map do |entry|
|
|
91
107
|
if File.directory?(entry)
|
|
92
108
|
Dir.glob(File.join(entry, "**", "*.rb"), sort: true)
|
|
@@ -97,6 +113,22 @@ module Rigor
|
|
|
97
113
|
end
|
|
98
114
|
end.uniq.freeze
|
|
99
115
|
end
|
|
116
|
+
|
|
117
|
+
# The `watch:` glob tuples covering the scan's entire input — one `[root, pattern]` per project `paths:`
|
|
118
|
+
# entry. A directory entry watches its whole `**/*.rb` subtree; a single-file `.rb` entry watches
|
|
119
|
+
# exactly that file (`[dir, basename]`). Roots are expanded to absolute paths by
|
|
120
|
+
# `Plugin::Base#watch_glob_entries`, so a relative CLI path (`app/models`) re-validates against the same
|
|
121
|
+
# tree from the project root on the next run. Co-extensive with {#scannable_paths}: every `.rb` the scan
|
|
122
|
+
# would read is watched, so any edit / addition / removal under those paths invalidates the cache.
|
|
123
|
+
def alias_watch_globs
|
|
124
|
+
services.configuration.paths.filter_map do |entry|
|
|
125
|
+
if File.directory?(entry)
|
|
126
|
+
[entry, "**/*.rb"]
|
|
127
|
+
elsif File.file?(entry) && entry.end_with?(".rb")
|
|
128
|
+
[File.dirname(entry), File.basename(entry)]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
100
132
|
end
|
|
101
133
|
|
|
102
134
|
Rigor::Plugin.register(DryTypes)
|
|
@@ -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
|
|
|
@@ -86,7 +88,7 @@ module Rigor
|
|
|
86
88
|
visit_factory(node, &)
|
|
87
89
|
return
|
|
88
90
|
end
|
|
89
|
-
node.
|
|
91
|
+
node.rigor_each_child { |child| walk_for_factories(child, &) }
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
def factory_call?(node)
|
|
@@ -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
|
|
@@ -65,7 +67,7 @@ module Rigor
|
|
|
65
67
|
case node
|
|
66
68
|
when Prism::DefNode then defs << node
|
|
67
69
|
when Prism::ClassNode, Prism::ModuleNode then nil # nested scopes own their own defs
|
|
68
|
-
else node.
|
|
70
|
+
else node.rigor_each_child { |child| collect_direct_defs(child, defs) }
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
@@ -122,6 +122,16 @@ module Rigor
|
|
|
122
122
|
carried_type(receiver_type, call_node.name)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# ADR-88 WD1 — the `dynamic_return` contribution is a PURE STRUCTURAL projection of the receiver's own
|
|
126
|
+
# carrier type args (`Result[T, E]#unwrap → T`), computed per call from the receiver type the engine
|
|
127
|
+
# already resolved; it reads no cross-file catalog. (The `Enum` nested-class emission rides the ADR-16
|
|
128
|
+
# macro substrate / synthetic-method scanner, a separate pre-pass, not this `dynamic_return`.) A stable
|
|
129
|
+
# sentinel declares "no cross-file fact surface", keeping the plugin incremental-capable; the
|
|
130
|
+
# `--verify-incremental` gate backstops the claim.
|
|
131
|
+
def incremental_state_fingerprint
|
|
132
|
+
"structural-carriers"
|
|
133
|
+
end
|
|
134
|
+
|
|
125
135
|
private
|
|
126
136
|
|
|
127
137
|
# @return [Rigor::Type, nil] the receiver's inferred type, or nil when the engine raises on a synthetic
|
|
@@ -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
|