rigortype 0.4.1 → 0.4.2
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 +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -7,6 +7,9 @@ require_relative "../inference/rbs_type_translator"
|
|
|
7
7
|
require_relative "../builtins/imported_refinements"
|
|
8
8
|
require_relative "rbs_hierarchy"
|
|
9
9
|
require_relative "member_consistency"
|
|
10
|
+
require_relative "required_features"
|
|
11
|
+
require_relative "gated_signature_guard"
|
|
12
|
+
require_relative "gated_signature_plan"
|
|
10
13
|
|
|
11
14
|
module Rigor
|
|
12
15
|
class Environment
|
|
@@ -83,28 +86,100 @@ module Rigor
|
|
|
83
86
|
# per-gem RBS bundled with Rigor itself is in scope for every analysis run. The gem stubs are
|
|
84
87
|
# intentionally read-only and appended LAST so user-supplied `signature_paths` win on name conflicts.
|
|
85
88
|
def build_env_for(libraries:, signature_paths:, virtual_rbs: [], deferred_signature_paths: [])
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
inputs = [signature_paths, virtual_rbs, deferred_signature_paths]
|
|
90
|
+
env, gated_dirs = whole_gated_attempt(libraries, inputs)
|
|
91
|
+
return env if gated_dirs.empty?
|
|
92
|
+
|
|
93
|
+
# Issue #1700 — a {RequiredFeatures}-gated directory joined; keep it whole only if it does not cost the
|
|
94
|
+
# environment anything ({.gated_failures}).
|
|
95
|
+
failed, quarantined = env ? gated_failures(env, gated_dirs, signature_paths, virtual_rbs) : [[], 1]
|
|
96
|
+
return env if failed.empty? && quarantined.zero?
|
|
97
|
+
|
|
98
|
+
tokens = gated_dirs.map { |dir| RequiredFeatures.token(RequiredFeatures.feature_for_dir(dir)) }
|
|
99
|
+
fallback, = build_env_attempt(libraries.reject { |library| tokens.include?(library.to_s) }, *inputs)
|
|
100
|
+
partial = partial_gated_env(libraries, inputs, gated_dirs, fallback, failed)
|
|
101
|
+
return partial if partial
|
|
102
|
+
|
|
103
|
+
trouble = failed.size + quarantined
|
|
104
|
+
return fallback if env.nil?
|
|
105
|
+
|
|
106
|
+
gated_trouble(fallback, gated_dirs, signature_paths, virtual_rbs) < trouble ? fallback : env
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The first attempt, with every activated gated directory whole. rbs 3.x validates a class's generic
|
|
110
|
+
# parameters and kind as each declaration is inserted, so a project `class Prime[T]` or `module Prime`
|
|
111
|
+
# raises out of this build where rbs 4.x lets the trial find it; with a gated directory active that is
|
|
112
|
+
# the directory's cost, not the environment's, and the build answers nil for the planned builds to
|
|
113
|
+
# replace. Without one the error is the environment's own and propagates as before.
|
|
114
|
+
def whole_gated_attempt(libraries, inputs)
|
|
115
|
+
build_env_attempt(libraries, *inputs)
|
|
116
|
+
rescue ::RBS::BaseError
|
|
117
|
+
gated_dirs = loadable_gated_dirs(libraries, library_loader(libraries).last)
|
|
118
|
+
raise if gated_dirs.empty?
|
|
119
|
+
|
|
120
|
+
[nil, gated_dirs]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Issue #1713 — the directory less what clashes ({GatedSignaturePlan}), planned against `fallback`, the
|
|
124
|
+
# environment without it. A plan whose trial build still fails for some gated type is re-planned once
|
|
125
|
+
# with each such type shelled — standing down as a whole — and nil is returned when that fails too, so the
|
|
126
|
+
# caller falls back to the whole directory standing down. Every input is one the env-cache key covers.
|
|
127
|
+
#
|
|
128
|
+
# @param failed — the gated types the whole directory failed to build for.
|
|
129
|
+
def partial_gated_env(libraries, inputs, gated_dirs, fallback, failed)
|
|
130
|
+
roots = gated_dirs.map { |dir| gated_dir_root(dir) }
|
|
131
|
+
plan = GatedSignaturePlan.build(roots, fallback)
|
|
132
|
+
if plan.changed
|
|
133
|
+
env = gated_plan_attempt(libraries, inputs, plan)
|
|
134
|
+
failed, quarantined = env ? gated_failures(env, gated_dirs, inputs[0], inputs[1]) : [failed, 0]
|
|
135
|
+
return env if env && failed.empty? && quarantined.zero?
|
|
93
136
|
end
|
|
137
|
+
return nil if failed.empty?
|
|
138
|
+
|
|
139
|
+
plan = GatedSignaturePlan.build(roots, fallback, failed.to_set { |name| name.to_s.delete_prefix("::") })
|
|
140
|
+
env = gated_plan_attempt(libraries, inputs, plan)
|
|
141
|
+
return nil if env.nil?
|
|
142
|
+
|
|
143
|
+
failed, quarantined = gated_failures(env, gated_dirs, inputs[0], inputs[1])
|
|
144
|
+
env if failed.empty? && quarantined.zero?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# A planned build is a candidate, never a requirement: one that raises leaves the caller's fallback.
|
|
148
|
+
def gated_plan_attempt(libraries, inputs, plan)
|
|
149
|
+
build_env_attempt(libraries, *inputs, gated_plan: plan).first
|
|
150
|
+
rescue ::RBS::BaseError
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def gated_dir_root(dir)
|
|
155
|
+
Pathname(File.join(VENDORED_GEM_SIGS_ROOT, dir))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# One environment build ({.build_env_for}'s body before the issue #1700 trial). Returns the environment
|
|
159
|
+
# and the gated vendored directories it loaded.
|
|
160
|
+
#
|
|
161
|
+
# @param gated_plan — issue #1713 — a {GatedSignaturePlan} loaded in place of the gated directories: its
|
|
162
|
+
# kept declarations where the directories would have loaded, its shells after the project's signatures.
|
|
163
|
+
def build_env_attempt(libraries, signature_paths, virtual_rbs, deferred_signature_paths, gated_plan: nil)
|
|
164
|
+
libraries = libraries_without_shadowed_bigdecimal_math(libraries)
|
|
165
|
+
rbs_loader, loaded_libraries = library_loader(libraries)
|
|
166
|
+
gated_dirs = loadable_gated_dirs(libraries, loaded_libraries)
|
|
94
167
|
# Project `signature_paths:` are loaded per-file by {.add_project_signatures} AFTER `from_loader`,
|
|
95
168
|
# NOT added to the loader here: `RBS::Environment.from_loader` parses every added file all-or-nothing,
|
|
96
169
|
# so one unparseable user `.rbs` raises `RBS::ParsingError` and collapses the WHOLE env to nil (every
|
|
97
170
|
# type-of query then degrades to `Dynamic[top]` — the "sig looks harmful" failure of the 2026-07-06
|
|
98
171
|
# mastodon coverage note). Per-file loading quarantines the broken file instead. Vendored / core-overlay
|
|
99
172
|
# sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
|
|
100
|
-
add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s))
|
|
173
|
+
add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s), gated_plan ? [] : gated_dirs)
|
|
101
174
|
env = unload_upstream_core_shims(RBS::Environment.from_loader(rbs_loader), rbs_loader)
|
|
175
|
+
gated_plan&.leading&.each { |source| add_parsed_decls(env, *source) }
|
|
102
176
|
project_files = project_sig_files(signature_paths)
|
|
103
177
|
# Issue #1075 — decided from the inputs before either side enters `env`: the consistency rule may
|
|
104
178
|
# stand the `.rbs` member down, and that has to happen before its file is added.
|
|
105
179
|
consistency = member_consistency_for(project_files, virtual_rbs)
|
|
106
180
|
add_project_signatures(env, signature_paths, deferred_signature_paths,
|
|
107
181
|
standdowns: consistency.signature_standdowns)
|
|
182
|
+
gated_plan&.shells&.each { |source| add_parsed_decls(env, *source) }
|
|
108
183
|
# Issue #928 — the capability-role catalog goes in AFTER the project's own signatures, per
|
|
109
184
|
# declaration, so a project that already declares one of these interface names keeps its own
|
|
110
185
|
# (`add_capability_role_signatures`). The rest of the bundled RBS cannot be ordered that way (it
|
|
@@ -119,7 +194,86 @@ module Rigor
|
|
|
119
194
|
# Issue #777 — resolve-time backstop: also unload colliding PROJECT buffers (class-vs-module /
|
|
120
195
|
# constant redeclarations against bundled RBS). Virtual culprits are preferred when both appear.
|
|
121
196
|
env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: project_files)
|
|
122
|
-
stub_missing_referenced_types(env, resolved, project_files)
|
|
197
|
+
[stub_missing_referenced_types(env, resolved, project_files), gated_dirs]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# An `RBS::EnvironmentLoader` with every library of `libraries` that resolves added, and those libraries.
|
|
201
|
+
def library_loader(libraries)
|
|
202
|
+
rbs_loader = RBS::EnvironmentLoader.new
|
|
203
|
+
# Issue #1700 — a `RequiredFeatures` token names a gated vendored directory, never an RBS library.
|
|
204
|
+
loaded_libraries = libraries.select do |library|
|
|
205
|
+
next false if RequiredFeatures.feature_of_token(library)
|
|
206
|
+
|
|
207
|
+
rbs_loader.has_library?(library: library, version: nil)
|
|
208
|
+
end
|
|
209
|
+
loaded_libraries.each do |library|
|
|
210
|
+
rbs_loader.add(library: library, version: nil)
|
|
211
|
+
end
|
|
212
|
+
[rbs_loader, loaded_libraries]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Issue #1700 — what loading the gated vendored `dirs` costs `env`: each type they declare whose instance
|
|
216
|
+
# or singleton definition fails to build, plus each project signature file or inline-synthesized RBS
|
|
217
|
+
# buffer quarantined for a duplicate declaration. A vendored copy joins signatures the project chose — its
|
|
218
|
+
# own `sig/`, a `signature_paths:` entry, a gem's `sig/`, an `rbs collection` copy, plugin signatures,
|
|
219
|
+
# inline RBS — and any clash with them (a member both declare, a class one side declares as a module,
|
|
220
|
+
# a different superclass or generic arity) surfaces here as a failed definition or a quarantine, and
|
|
221
|
+
# degrades the whole class to `Dynamic[top]` or drops the project's file. A trial build is the oracle
|
|
222
|
+
# rather than a comparison of declarations because it answers exactly what RBS itself decides — an
|
|
223
|
+
# overload continuation (`| ...`), a superclass written relative or absolute, a type parameter — with no
|
|
224
|
+
# second reading of the rules to drift. It runs only when a gated directory joined, on an env-cache miss.
|
|
225
|
+
def gated_trouble(env, dirs, signature_paths, virtual_rbs)
|
|
226
|
+
failed, quarantined = gated_failures(env, dirs, signature_paths, virtual_rbs)
|
|
227
|
+
failed.size + quarantined
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# {.gated_trouble} split in two: the gated type names whose definitions fail, and the number of quarantined
|
|
231
|
+
# signature sources that declare one. Issue #1713 re-plans against the first.
|
|
232
|
+
def gated_failures(env, dirs, signature_paths, virtual_rbs)
|
|
233
|
+
type_names = gated_type_names(dirs)
|
|
234
|
+
builder = RBS::DefinitionBuilder.new(env: env)
|
|
235
|
+
failed = type_names.select do |type_name|
|
|
236
|
+
entry = env.class_decls[type_name]
|
|
237
|
+
next false if entry.nil?
|
|
238
|
+
|
|
239
|
+
# rbs 4 validates the declarations' type parameters here (`GenericParameterMismatchError`), which
|
|
240
|
+
# the definition builds below do not reach on their own.
|
|
241
|
+
entry.type_params if entry.respond_to?(:type_params)
|
|
242
|
+
builder.build_instance(type_name)
|
|
243
|
+
builder.build_singleton(type_name)
|
|
244
|
+
false
|
|
245
|
+
rescue StandardError
|
|
246
|
+
true
|
|
247
|
+
end
|
|
248
|
+
declares = gated_declaration_pattern(type_names)
|
|
249
|
+
quarantined = collision_quarantined_project_signatures(env, signature_paths).count do |file, _note|
|
|
250
|
+
File.read(file).match?(declares)
|
|
251
|
+
rescue SystemCallError, IOError, ArgumentError
|
|
252
|
+
false
|
|
253
|
+
end
|
|
254
|
+
[failed, quarantined + quarantined_virtual_count(env, virtual_rbs, declares)]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# A `class` / `module` line declaring one of the gated types, by its last name segment. A quarantine is
|
|
258
|
+
# counted only for a file that declares one, so a project whose other signature files were already
|
|
259
|
+
# quarantined (a duplicated gem `sig/`) does not read every gated directory as costly and pay a second
|
|
260
|
+
# build on each env-cache miss.
|
|
261
|
+
def gated_declaration_pattern(type_names)
|
|
262
|
+
segments = type_names.map { |name| Regexp.escape(name.name.to_s) }.uniq.join("|")
|
|
263
|
+
/^\s*(?:class|module)\s+(?:::)?(?:[\w:]*::)?(?:#{segments})\b/
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def gated_type_names(dirs)
|
|
267
|
+
roots = dirs.map { |dir| gated_dir_root(dir) }
|
|
268
|
+
names = roots.flat_map { |root| GatedSignatureGuard.type_names(root) }
|
|
269
|
+
names.uniq.map { |name| ::RBS::TypeName.parse("::#{name}") }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def quarantined_virtual_count(env, virtual_rbs, declares)
|
|
273
|
+
present = env.buffers.to_set(&:name)
|
|
274
|
+
virtual_rbs.count do |name, content|
|
|
275
|
+
!content.empty? && !present.include?(name) && content.match?(declares) && parseable_rbs?(content)
|
|
276
|
+
end
|
|
123
277
|
end
|
|
124
278
|
|
|
125
279
|
# rbs ships `stdlib/bigdecimal/` and `stdlib/bigdecimal-math/` as two libraries, so
|
|
@@ -1228,6 +1382,7 @@ module Rigor
|
|
|
1228
1382
|
# Keyed by `data/vendored_gem_sigs/` directory basename.
|
|
1229
1383
|
LIBRARY_SUPPLEMENT_VENDORED_DIRS = {
|
|
1230
1384
|
"cgi" => "cgi",
|
|
1385
|
+
"prime" => "singleton",
|
|
1231
1386
|
"prism" => "prism"
|
|
1232
1387
|
}.freeze
|
|
1233
1388
|
private_constant :LIBRARY_SUPPLEMENT_VENDORED_DIRS
|
|
@@ -1258,13 +1413,13 @@ module Rigor
|
|
|
1258
1413
|
# overlay is added per-file, not per-directory, because the `LIBRARY_SUPPLEMENT_CORE_OVERLAYS` and
|
|
1259
1414
|
# `RBS_LINE_CORE_OVERLAYS` files must be gated individually.
|
|
1260
1415
|
#
|
|
1416
|
+
# A directory {RequiredFeatures} gates loads only when the library list activated it (issue #1700).
|
|
1417
|
+
#
|
|
1261
1418
|
# @param loaded_library_names — libraries that actually resolved on this loader.
|
|
1262
|
-
|
|
1419
|
+
# @param gated_dirs — the {RequiredFeatures}-gated directory basenames this library list activates.
|
|
1420
|
+
def add_bundled_signatures(rbs_loader, loaded_library_names, gated_dirs = [])
|
|
1263
1421
|
vendored_gem_sig_paths.each do |path|
|
|
1264
|
-
|
|
1265
|
-
next unless supplement_dependency_loaded?(LIBRARY_SUPPLEMENT_VENDORED_DIRS, path, loaded_library_names)
|
|
1266
|
-
|
|
1267
|
-
rbs_loader.add(path: path)
|
|
1422
|
+
rbs_loader.add(path: path) if vendored_dir_loads?(path, loaded_library_names, gated_dirs)
|
|
1268
1423
|
end
|
|
1269
1424
|
core_overlay_sig_paths.each do |dir|
|
|
1270
1425
|
next unless dir.directory?
|
|
@@ -1279,6 +1434,31 @@ module Rigor
|
|
|
1279
1434
|
end
|
|
1280
1435
|
end
|
|
1281
1436
|
|
|
1437
|
+
# Issue #1700 — the {RequiredFeatures}-gated directories `libraries` activates, less one whose feature
|
|
1438
|
+
# resolved as an RBS library here (`libraries: [prime]` on a host whose `prime` gem ships its `sig/`),
|
|
1439
|
+
# whose own copy is then already loaded. {.build_env_for} then keeps a directory that joined only if the
|
|
1440
|
+
# trial build finds it costs nothing ({.gated_trouble}). Both decisions are made inside the build, against
|
|
1441
|
+
# what the environment holds, so the env-cache producer — which rebuilds through it from the same inputs
|
|
1442
|
+
# — decides the same way.
|
|
1443
|
+
def loadable_gated_dirs(libraries, loaded_libraries)
|
|
1444
|
+
resolved = loaded_libraries.to_set(&:to_s)
|
|
1445
|
+
RequiredFeatures.active_dirs(libraries.map(&:to_s)).reject do |dir|
|
|
1446
|
+
resolved.include?(RequiredFeatures.feature_for_dir(dir))
|
|
1447
|
+
end
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
# @param path — a `data/vendored_gem_sigs/<gem>/` entry.
|
|
1451
|
+
# @return true when the directory exists, its supplemented library (if any) loaded, and — for a
|
|
1452
|
+
# {RequiredFeatures}-gated directory — the library list activated it.
|
|
1453
|
+
def vendored_dir_loads?(path, loaded_library_names, gated_dirs)
|
|
1454
|
+
return false unless path.directory?
|
|
1455
|
+
return false unless supplement_dependency_loaded?(LIBRARY_SUPPLEMENT_VENDORED_DIRS, path,
|
|
1456
|
+
loaded_library_names)
|
|
1457
|
+
|
|
1458
|
+
basename = path.basename.to_s
|
|
1459
|
+
!RequiredFeatures.gated_dir?(basename) || gated_dirs.include?(basename)
|
|
1460
|
+
end
|
|
1461
|
+
|
|
1282
1462
|
# @param supplements — basename → gating library map.
|
|
1283
1463
|
# @param path — the vendored directory or overlay file to test.
|
|
1284
1464
|
# @param loaded_library_names — libraries that actually resolved on this loader.
|
|
@@ -1537,6 +1717,32 @@ module Rigor
|
|
|
1537
1717
|
@state[:signature_standdowns] ||= self.class.deferred_standdowns(env, @deferred_signature_paths).freeze
|
|
1538
1718
|
end
|
|
1539
1719
|
|
|
1720
|
+
# Issue #1713 — what stood down from each {RequiredFeatures}-gated vendored directory this loader's library
|
|
1721
|
+
# list activated, as {GatedSignaturePlan.standdowns} reports it: `[dir, [[declaration, cause_file, reason],
|
|
1722
|
+
# ...], whole]`. Derived from the final env, so a cache HIT reports what the cold build decided. Empty, without
|
|
1723
|
+
# building the env, when no gated directory is active; empty too for a directory whose feature resolved as
|
|
1724
|
+
# an RBS library, which the build never loads ({.loadable_gated_dirs}).
|
|
1725
|
+
def vendored_standdowns
|
|
1726
|
+
@state[:vendored_standdowns] ||= derive_vendored_standdowns.freeze
|
|
1727
|
+
end
|
|
1728
|
+
|
|
1729
|
+
def derive_vendored_standdowns
|
|
1730
|
+
names = @libraries.map(&:to_s)
|
|
1731
|
+
roots = RequiredFeatures.active_dirs(names).uniq.each_with_object({}) do |dir, acc|
|
|
1732
|
+
acc[dir] = self.class.gated_dir_root(dir) unless gated_feature_resolved?(dir, names)
|
|
1733
|
+
end
|
|
1734
|
+
return [] if roots.empty?
|
|
1735
|
+
|
|
1736
|
+
GatedSignaturePlan.standdowns(env, roots, self.class.project_sig_files(@signature_paths))
|
|
1737
|
+
end
|
|
1738
|
+
private :derive_vendored_standdowns
|
|
1739
|
+
|
|
1740
|
+
def gated_feature_resolved?(dir, library_names)
|
|
1741
|
+
feature = RequiredFeatures.feature_for_dir(dir)
|
|
1742
|
+
library_names.include?(feature) && RBS::EnvironmentLoader.new.has_library?(library: feature, version: nil)
|
|
1743
|
+
end
|
|
1744
|
+
private :gated_feature_resolved?
|
|
1745
|
+
|
|
1540
1746
|
# Issue #735 — the class / module names whose PRIMARY declaration lives under the project's own
|
|
1541
1747
|
# `signature_paths:`, top-level prefix stripped ("::Admin::Widget" reads back as "Admin::Widget").
|
|
1542
1748
|
#
|
|
@@ -1807,6 +2013,69 @@ module Rigor
|
|
|
1807
2013
|
false
|
|
1808
2014
|
end
|
|
1809
2015
|
|
|
2016
|
+
# Issue #1698 — the modules and classes `name`'s RBS declarations place in its ancestry, read from the
|
|
2017
|
+
# declarations themselves rather than through the definition builder, so a declaration whose definition
|
|
2018
|
+
# fails to build (a missing mixin, a superclass mismatch) still yields what it declares: each declaration's
|
|
2019
|
+
# `include` / `prepend` members and a class's superclass, followed through the other RBS-declared modules
|
|
2020
|
+
# and classes they name, `::`-stripped and without `name` itself. `[names, complete]`: `complete` is false
|
|
2021
|
+
# when a name on the way is not an RBS class or module (an alias, an undeclared mixin), when the walk
|
|
2022
|
+
# passes 64 names, or when the environment is unavailable; `names` is then what was read so far. Interface
|
|
2023
|
+
# includes own no constants and are skipped. Memoised per loader.
|
|
2024
|
+
def declared_ancestry(name)
|
|
2025
|
+
memo = (@state[:declared_ancestry] ||= {})
|
|
2026
|
+
key = name.to_s.delete_prefix("::")
|
|
2027
|
+
memo[key] ||= build_declared_ancestry(key).freeze
|
|
2028
|
+
end
|
|
2029
|
+
|
|
2030
|
+
def build_declared_ancestry(key)
|
|
2031
|
+
environment = env
|
|
2032
|
+
return [[].freeze, false] if environment.nil?
|
|
2033
|
+
|
|
2034
|
+
names = []
|
|
2035
|
+
complete = true
|
|
2036
|
+
pending = [key]
|
|
2037
|
+
until pending.empty?
|
|
2038
|
+
current = pending.shift
|
|
2039
|
+
entry = environment.class_decls[parse_type_name(current)]
|
|
2040
|
+
if entry.nil?
|
|
2041
|
+
complete = false
|
|
2042
|
+
next
|
|
2043
|
+
end
|
|
2044
|
+
declared_parents(entry).each do |parent|
|
|
2045
|
+
next if parent == key || names.include?(parent)
|
|
2046
|
+
|
|
2047
|
+
names << parent
|
|
2048
|
+
pending << parent
|
|
2049
|
+
end
|
|
2050
|
+
next unless names.size > 64
|
|
2051
|
+
|
|
2052
|
+
complete = false
|
|
2053
|
+
break
|
|
2054
|
+
end
|
|
2055
|
+
[names.freeze, complete]
|
|
2056
|
+
rescue ::RBS::BaseError
|
|
2057
|
+
[[].freeze, false]
|
|
2058
|
+
end
|
|
2059
|
+
private :build_declared_ancestry
|
|
2060
|
+
|
|
2061
|
+
def declared_parents(entry)
|
|
2062
|
+
self.class.entry_declarations(entry).flat_map do |decl|
|
|
2063
|
+
parents = decl.respond_to?(:members) ? decl.members.filter_map { |member| mixin_parent(member) } : []
|
|
2064
|
+
superclass = decl.respond_to?(:super_class) ? decl.super_class : nil
|
|
2065
|
+
parents << superclass.name.to_s.delete_prefix("::") if superclass
|
|
2066
|
+
parents
|
|
2067
|
+
end
|
|
2068
|
+
end
|
|
2069
|
+
private :declared_parents
|
|
2070
|
+
|
|
2071
|
+
def mixin_parent(member)
|
|
2072
|
+
return nil unless member.is_a?(::RBS::AST::Members::Include) || member.is_a?(::RBS::AST::Members::Prepend)
|
|
2073
|
+
return nil if member.name.interface?
|
|
2074
|
+
|
|
2075
|
+
member.name.to_s.delete_prefix("::")
|
|
2076
|
+
end
|
|
2077
|
+
private :mixin_parent
|
|
2078
|
+
|
|
1810
2079
|
# Yields every known class / module / alias name (top-level prefixed) currently loaded into the
|
|
1811
2080
|
# environment. The cache producer that materialises the known-name set uses this so it never recurses
|
|
1812
2081
|
# back through {#class_known?}.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analysis/path_expansion"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class Environment
|
|
7
|
+
# Issue #1700 — vendored signatures that load only when the project's own source requires the feature. A
|
|
8
|
+
# `data/vendored_gem_sigs/` directory loads for every project, which is safe for a gem that owns its
|
|
9
|
+
# namespace (`PG::Connection`) and unsafe for one that reopens core classes or claims a short top-level name.
|
|
10
|
+
# `prime` does both: it declares `class Prime` and adds `Integer#prime?`, and with it loaded unconditionally a
|
|
11
|
+
# project's own `class Prime` with a zero-argument `prime?` drew a false `call.wrong-arity` against the gem's
|
|
12
|
+
# `(Integer, ?generator)`. Such a directory is listed in {VENDORED_DIRS} and loads only when some project
|
|
13
|
+
# source file requires the feature ({PATTERN}), or the configuration lists it under `libraries:`.
|
|
14
|
+
#
|
|
15
|
+
# The decision travels as a library token ({token}) inside the environment's library list, so every
|
|
16
|
+
# consumer that already keys on that list sees it without plumbing of its own: the RBS environment cache
|
|
17
|
+
# key (`Cache::RbsDescriptor.libraries_entry`), the env-cache producer that rebuilds from
|
|
18
|
+
# `RbsLoader#libraries`, and the run-result key built from the loader's descriptor. The ADR-87 boot-slimming
|
|
19
|
+
# probe reconstructs the list from configuration alone and so never carries a token: for a project that
|
|
20
|
+
# requires a listed feature it misses and the full path serves the run, which forgoes the fast lane and
|
|
21
|
+
# never serves a wrong answer. Whether a token's directory then loads is `RbsLoader`'s decision, made
|
|
22
|
+
# against what the environment actually holds (`RbsLoader.gated_dir_loads?`).
|
|
23
|
+
#
|
|
24
|
+
# A leaf file (no engine requires), for the reason `default_libraries.rb` is one: the incremental snapshot
|
|
25
|
+
# fingerprint reads it without building an environment.
|
|
26
|
+
module RequiredFeatures
|
|
27
|
+
# Required feature => `data/vendored_gem_sigs/` directory basename.
|
|
28
|
+
VENDORED_DIRS = { "prime" => "prime" }.freeze
|
|
29
|
+
|
|
30
|
+
TOKEN_PREFIX = "rigor-vendored:"
|
|
31
|
+
|
|
32
|
+
FEATURE_ALTERNATION = VENDORED_DIRS.keys.map { |feature| Regexp.escape(feature) }.join("|").freeze
|
|
33
|
+
private_constant :FEATURE_ALTERNATION
|
|
34
|
+
|
|
35
|
+
# A `require` call naming a listed feature as a string literal, with or without parentheses: the bare call
|
|
36
|
+
# and `Kernel.require` count, a `require` that follows any other `.` (`obj.require`) or is part of a longer
|
|
37
|
+
# word (`require_relative`) does not, and a computed name never does; nor do `require(` with the name on
|
|
38
|
+
# the next line or `send(:require, "prime")`. A match on a whole-line comment ({#commented?}) does not
|
|
39
|
+
# count, so commenting a require out changes nothing. Otherwise the match is textual and leans toward
|
|
40
|
+
# loading: a `require "prime"` in a trailing comment, a heredoc or an `=begin` block counts. That direction
|
|
41
|
+
# only loads the gem's own signatures, and `RbsLoader` still drops them when they cost the environment
|
|
42
|
+
# anything.
|
|
43
|
+
PATTERN = /(?:(?<![\w.$@])|(?<=Kernel\.))require[ \t]*\(?[ \t]*(["'])(#{FEATURE_ALTERNATION})\1/
|
|
44
|
+
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# The features a run over `configuration` requires: every file under the configured paths, plus
|
|
48
|
+
# `extra_files` (a run's own targets or a probe's file, which may lie outside them). The scan does not
|
|
49
|
+
# depend on which files a run checks, so `rigor check lib/a.rb`, a probe on `lib/a.rb` and the language
|
|
50
|
+
# server all see a `require "prime"` in `lib/b.rb`. Fails soft to no features.
|
|
51
|
+
#
|
|
52
|
+
# @param sources — path => source text, read in place of that path's file (an editor buffer).
|
|
53
|
+
def for_configuration(configuration, extra_files = [], sources: {})
|
|
54
|
+
files = Analysis::PathExpansion.ruby_files(configuration.paths, configuration.exclude_patterns)
|
|
55
|
+
scan(files + Array(extra_files).map(&:to_s), sources: sources)
|
|
56
|
+
rescue StandardError
|
|
57
|
+
[]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param files — source paths (Strings or Pathnames); an unreadable one contributes nothing.
|
|
61
|
+
# @param sources — path => source text, scanned instead of the file; its paths are scanned even when
|
|
62
|
+
# `files` does not list them.
|
|
63
|
+
# @return the sorted, frozen features some file requires.
|
|
64
|
+
def scan(files, sources: {})
|
|
65
|
+
sources = sources.to_h { |path, text| [File.expand_path(path.to_s), text] }
|
|
66
|
+
paths = (Array(files).map { |path| source_key(File.expand_path(path.to_s), sources) } + sources.keys).uniq
|
|
67
|
+
found = Set.new
|
|
68
|
+
paths.each do |path|
|
|
69
|
+
break if found.size == VENDORED_DIRS.size
|
|
70
|
+
|
|
71
|
+
found.merge(sources.key?(path) ? features_of(sources[path]) : memoized_features_in(path))
|
|
72
|
+
end
|
|
73
|
+
prune_memo(paths)
|
|
74
|
+
found.to_a.sort.freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return path (absolute) => the features that file requires, for every file under the configured paths
|
|
78
|
+
# that requires any. A long-lived reader (the language server) keeps this and overlays its open buffers.
|
|
79
|
+
def feature_files(configuration)
|
|
80
|
+
listed = Analysis::PathExpansion.ruby_files(configuration.paths, configuration.exclude_patterns)
|
|
81
|
+
files = listed.map { |path| File.expand_path(path.to_s) }
|
|
82
|
+
result = files.each_with_object({}) do |path, acc|
|
|
83
|
+
features = memoized_features_in(path)
|
|
84
|
+
acc[path] = features unless features.empty?
|
|
85
|
+
end
|
|
86
|
+
prune_memo(files)
|
|
87
|
+
result
|
|
88
|
+
rescue StandardError
|
|
89
|
+
{}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Paths are compared expanded, so a relative `--instead-of=lib/a.rb` replaces the configured
|
|
93
|
+
# `/project/lib/a.rb`; a source whose real path matches (a symlinked directory) replaces it too. The real
|
|
94
|
+
# path is asked only for a file that shares a source's basename, so a scan does not resolve every path.
|
|
95
|
+
def source_key(path, sources)
|
|
96
|
+
return path if sources.empty? || sources.key?(path)
|
|
97
|
+
|
|
98
|
+
candidates = sources.keys.select { |key| File.basename(key) == File.basename(path) }
|
|
99
|
+
return path if candidates.empty?
|
|
100
|
+
|
|
101
|
+
real = real_path(path)
|
|
102
|
+
candidates.find { |key| real_path(key) == real } || path
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def real_path(path)
|
|
106
|
+
File.realpath(path)
|
|
107
|
+
rescue SystemCallError
|
|
108
|
+
path
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# One run scans the same files more than once (the incremental snapshot fingerprint, then the environment
|
|
112
|
+
# build; a long-lived language server on every rebuild), so on the main Ractor each file's answer is kept
|
|
113
|
+
# against its stat and the file is re-read only when the stat moved. A non-main Ractor may not touch the
|
|
114
|
+
# module's state and reads the file every time.
|
|
115
|
+
def memoized_features_in(path)
|
|
116
|
+
return features_in(path) unless Ractor.current == Ractor.main
|
|
117
|
+
|
|
118
|
+
stat = File.stat(path)
|
|
119
|
+
stamp = [stat.mtime.to_r, stat.ctime.to_r, stat.size, stat.ino]
|
|
120
|
+
memo = (@memo ||= {})
|
|
121
|
+
cached = memo[path]
|
|
122
|
+
return cached.last if cached && cached.first == stamp
|
|
123
|
+
|
|
124
|
+
features = features_in(path)
|
|
125
|
+
memo[path] = [stamp, features]
|
|
126
|
+
features
|
|
127
|
+
rescue SystemCallError
|
|
128
|
+
[]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Keeps the memo to the files the latest scan was asked about, so a long-lived language server holds one
|
|
132
|
+
# entry per project file rather than one per file it ever saw.
|
|
133
|
+
def prune_memo(paths)
|
|
134
|
+
return unless Ractor.current == Ractor.main && @memo && @memo.size > paths.size
|
|
135
|
+
|
|
136
|
+
keep = paths.to_set
|
|
137
|
+
@memo.select! { |path, _| keep.include?(path) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return the features one file requires.
|
|
141
|
+
def features_in(path)
|
|
142
|
+
features_of(File.binread(path))
|
|
143
|
+
rescue SystemCallError, IOError
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @return the features `source` requires.
|
|
148
|
+
def features_of(source)
|
|
149
|
+
# A plain substring test first: almost no file mentions any listed feature, and it is far cheaper than
|
|
150
|
+
# the regexp on a file that does not.
|
|
151
|
+
return [] unless VENDORED_DIRS.each_key.any? { |feature| source.include?(feature) }
|
|
152
|
+
|
|
153
|
+
bytes = source.b
|
|
154
|
+
named = []
|
|
155
|
+
position = 0
|
|
156
|
+
while (match = PATTERN.match(bytes, position))
|
|
157
|
+
named << match[2] unless commented?(bytes, match.begin(0))
|
|
158
|
+
position = match.end(0)
|
|
159
|
+
end
|
|
160
|
+
# The table's own frozen keys, so the result is shareable across Ractors whatever the source's encoding.
|
|
161
|
+
VENDORED_DIRS.each_key.select { |feature| named.include?(feature) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Whether the match at `offset` sits on a line whose first non-blank character is `#`: a whole-line comment.
|
|
165
|
+
# Deliberately narrower than "after a `#`": a `#` later on a line may be inside `?#`, `%q(#)`, `%w[a#b]`,
|
|
166
|
+
# a regexp or a string continued from an earlier line, and reading it as a comment would drop a real
|
|
167
|
+
# require — a false `call.undefined-method` on every call into the feature. A trailing comment
|
|
168
|
+
# (`p 1 # require "prime"`) therefore still counts, which only loads the gem's own signatures.
|
|
169
|
+
def commented?(bytes, offset)
|
|
170
|
+
line_start = offset.zero? ? 0 : (bytes.rindex("\n", offset - 1) || -1) + 1
|
|
171
|
+
bytes.byteslice(line_start, offset - line_start).lstrip.start_with?("#")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The library tokens for `features` and for any listed feature the configuration's `libraries:` names.
|
|
175
|
+
def tokens(features, configured_libraries)
|
|
176
|
+
configured = Array(configured_libraries).map(&:to_s).select { |name| VENDORED_DIRS.key?(name) }
|
|
177
|
+
(Array(features) | configured).sort.map { |feature| token(feature) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def token(feature)
|
|
181
|
+
"#{TOKEN_PREFIX}#{feature}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The feature a library token names, or nil for an ordinary library name.
|
|
185
|
+
def feature_of_token(name)
|
|
186
|
+
name = name.to_s
|
|
187
|
+
name.start_with?(TOKEN_PREFIX) ? name.delete_prefix(TOKEN_PREFIX) : nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The vendored directory basenames a library list activates.
|
|
191
|
+
def active_dirs(library_names)
|
|
192
|
+
library_names.filter_map { |name| VENDORED_DIRS[feature_of_token(name)] }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Whether `dir_basename` is one of the gated directories.
|
|
196
|
+
def gated_dir?(dir_basename)
|
|
197
|
+
VENDORED_DIRS.value?(dir_basename)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The feature a gated directory loads for.
|
|
201
|
+
def feature_for_dir(dir_basename)
|
|
202
|
+
VENDORED_DIRS.key(dir_basename)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
5
|
require_relative "environment/default_libraries"
|
|
6
|
+
require_relative "environment/required_features"
|
|
6
7
|
require_relative "environment/class_registry"
|
|
7
8
|
require_relative "environment/rbs_loader"
|
|
8
9
|
require_relative "environment/reflection"
|
|
@@ -275,7 +276,7 @@ module Rigor
|
|
|
275
276
|
bundler_lockfile: nil,
|
|
276
277
|
rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
|
|
277
278
|
synthetic_method_index: nil, project_patched_methods: nil,
|
|
278
|
-
source_files: [], locked_gems: nil)
|
|
279
|
+
source_files: [], locked_gems: nil, required_features: nil)
|
|
279
280
|
resolved_paths = project_signature_roots(signature_paths, root: root)
|
|
280
281
|
# O4 MVP — append per-gem `sig/` directories discovered under the target project's bundler install
|
|
281
282
|
# root. Empty array when neither an explicit path nor auto-detection finds a bundle. Order: user
|
|
@@ -314,8 +315,10 @@ module Rigor
|
|
|
314
315
|
# and the `data/vendored_gem_sigs/` bundle (`redis`, `nokogiri`, `pg`, …). `skip_gem_names:` passes
|
|
315
316
|
# both sets so the collection copy doesn't double-declare against rigor's bundled RBS (the
|
|
316
317
|
# `RBS::DuplicatedDeclarationError` hazard).
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
# Issue #1700 — plus the gated vendored directories this run's source activates ({#feature_tokens}).
|
|
319
|
+
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s) +
|
|
320
|
+
feature_tokens(required_features, source_files, libraries)).uniq
|
|
321
|
+
skip_gem_names = collection_skip_gem_names(merged_libraries)
|
|
319
322
|
collection_paths = RbsCollectionDiscovery.discover(
|
|
320
323
|
lockfile_path: rbs_collection_lockfile,
|
|
321
324
|
project_root: root,
|
|
@@ -400,6 +403,21 @@ module Rigor
|
|
|
400
403
|
|
|
401
404
|
private
|
|
402
405
|
|
|
406
|
+
# Issue #1700 — the `RequiredFeatures` library tokens for the gated vendored directories this run asks for:
|
|
407
|
+
# each feature its source requires and each one the configuration's `libraries:` names. `required_features`
|
|
408
|
+
# is the scan a caller already took; nil scans `source_files`. Whether a directory then loads is decided
|
|
409
|
+
# by `RbsLoader.build_env_for` against the environment it builds.
|
|
410
|
+
def feature_tokens(required_features, source_files, libraries)
|
|
411
|
+
RequiredFeatures.tokens(required_features || RequiredFeatures.scan(source_files), libraries)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# The gem names an `rbs collection` copy must not double-declare against: the library list and every
|
|
415
|
+
# vendored directory except a `RequiredFeatures`-gated one. A gated directory stands down for any other
|
|
416
|
+
# copy of its declarations (`GatedSignatureGuard`), so the collection's copy is kept and wins.
|
|
417
|
+
def collection_skip_gem_names(library_names)
|
|
418
|
+
library_names + RbsLoader.vendored_gem_names.reject { |name| RequiredFeatures.gated_dir?(name) }
|
|
419
|
+
end
|
|
420
|
+
|
|
403
421
|
def default_signature_paths(root)
|
|
404
422
|
sig = Pathname(root) / DEFAULT_PROJECT_SIG_DIR
|
|
405
423
|
sig.directory? ? [sig] : []
|