rigortype 0.4.1 → 0.4.3
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 +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- 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 +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- 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/effects_cache_probe.rb +1 -1
- 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 +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- 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 +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- 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 +286 -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/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -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/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- 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_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -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 +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- 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 +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- 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-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -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 +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -1
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
|
|
6
6
|
require_relative "../incremental_run_slot"
|
|
7
|
+
require_relative "../dependency_source_inference/gem_resolver"
|
|
7
8
|
require_relative "../../cache/file_digest"
|
|
8
9
|
require_relative "../../cache/incremental_snapshot"
|
|
9
10
|
|
|
@@ -61,7 +62,9 @@ module Rigor
|
|
|
61
62
|
# signature file under the configured or auto-detected `sig/` roots, or a `pre_eval:` file, changed in
|
|
62
63
|
# between would leave the snapshot the run restores keyed by one tree
|
|
63
64
|
# and the run reading another. From the stamp on, the lockfiles are the guard's to watch ({#admits?}), which
|
|
64
|
-
# is also what lets the slot's key, whose only file inputs they are, be computed when the run ends.
|
|
65
|
+
# is also what lets the slot's key, whose only file inputs they are, be computed when the run ends. The
|
|
66
|
+
# opted-in gems' resolved versions, the key's one input that is neither a file nor the configuration (issue
|
|
67
|
+
# #1814), are read at the mark and watched the same way.
|
|
65
68
|
#
|
|
66
69
|
# The mark also records which of the paths an existence row can name are present: the lockfiles, the
|
|
67
70
|
# analysis roots, the `pre_eval:` entries and the signature roots.
|
|
@@ -78,10 +81,22 @@ module Rigor
|
|
|
78
81
|
presence = (lockfiles + named.map { |path| File.absolute_path(path.to_s) }).to_h do |path|
|
|
79
82
|
[path, File.exist?(path)]
|
|
80
83
|
end
|
|
84
|
+
rows = dependency_rows(configuration)
|
|
85
|
+
return nil if rows.nil?
|
|
86
|
+
|
|
81
87
|
new(started_ns: started_ns, device: device, lockfiles: lockfiles, presence: presence,
|
|
82
|
-
stamp_dir: stamp_dir(cache_root))
|
|
88
|
+
stamp_dir: stamp_dir(cache_root), configuration: configuration, dependency_rows: rows)
|
|
83
89
|
end
|
|
84
90
|
|
|
91
|
+
# Issue #1814 — the opted-in gems' `(name, version, mode)` rows as the key carries them; nil when they cannot
|
|
92
|
+
# be resolved, which takes no mark (the key could not be computed either).
|
|
93
|
+
def self.dependency_rows(configuration)
|
|
94
|
+
DependencySourceInference::GemResolver.dependency_entries(configuration.dependencies)
|
|
95
|
+
rescue StandardError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
private_class_method :dependency_rows
|
|
99
|
+
|
|
85
100
|
# The mark: the change time and device of the first stamp whose change time is later than that of a
|
|
86
101
|
# stamp taken before it, on the same device; nil when none is written within {TICK_WAIT_LIMIT}.
|
|
87
102
|
def self.stamp(cache_root)
|
|
@@ -143,17 +158,21 @@ module Rigor
|
|
|
143
158
|
end
|
|
144
159
|
private_class_method :lockfile_paths
|
|
145
160
|
|
|
146
|
-
def initialize(started_ns:, device:, lockfiles:, presence: {}, stamp_dir: nil
|
|
161
|
+
def initialize(started_ns:, device:, lockfiles:, presence: {}, stamp_dir: nil, configuration: nil,
|
|
162
|
+
dependency_rows: [])
|
|
147
163
|
@stamp_dir = stamp_dir
|
|
148
164
|
@started_ns = started_ns
|
|
149
165
|
@device = device
|
|
150
166
|
@lockfiles = lockfiles
|
|
151
167
|
@presence = presence
|
|
168
|
+
@configuration = configuration
|
|
169
|
+
@dependency_rows = dependency_rows
|
|
152
170
|
end
|
|
153
171
|
|
|
154
172
|
# True when nothing the guard watches moved since the mark:
|
|
155
173
|
#
|
|
156
|
-
# - no lockfile the key or the fingerprint digests was written, created or removed
|
|
174
|
+
# - no lockfile the key or the fingerprint digests was written, created or removed, and no opted-in gem
|
|
175
|
+
# resolves to another version than at the mark (issue #1814);
|
|
157
176
|
# - no file `rows` or `pinned` names changed. A content row's file changed if its change time moved. An
|
|
158
177
|
# existence row asks only whether its path is there, so a path the mark recorded changed if it came or
|
|
159
178
|
# went, and an editor's lock file created and removed beside it, which moves its directory's change
|
|
@@ -174,7 +193,7 @@ module Rigor
|
|
|
174
193
|
# @param rows — a {Cache::Descriptor} of the rows the key does not pin
|
|
175
194
|
# @param pinned — a {Cache::Descriptor} of the rows it does
|
|
176
195
|
def admits?(rows, pinned: Cache::Descriptor.new)
|
|
177
|
-
!clock_stepped_back? && @lockfiles.none? { |path| lockfile_changed?(path) } &&
|
|
196
|
+
!clock_stepped_back? && @lockfiles.none? { |path| lockfile_changed?(path) } && dependency_rows_unchanged? &&
|
|
178
197
|
files_unchanged?(rows.files, strict: true) && globs_unchanged?(rows.globs, strict: true) &&
|
|
179
198
|
files_unchanged?(pinned.files, strict: false) && globs_unchanged?(pinned.globs, strict: false)
|
|
180
199
|
rescue StandardError
|
|
@@ -196,6 +215,12 @@ module Rigor
|
|
|
196
215
|
|
|
197
216
|
private
|
|
198
217
|
|
|
218
|
+
# Issue #1814 — the opted-in gems resolve to the versions they did at the mark. A guard built without a
|
|
219
|
+
# configuration (the specs' bare constructor) watches none.
|
|
220
|
+
def dependency_rows_unchanged?
|
|
221
|
+
@configuration.nil? || self.class.send(:dependency_rows, @configuration) == @dependency_rows
|
|
222
|
+
end
|
|
223
|
+
|
|
199
224
|
# One more stamp: a change time earlier than the mark means the clock stepped back since it was taken, so
|
|
200
225
|
# a save since then may carry a change time the guard reads as before it. A stamp that cannot be written,
|
|
201
226
|
# or lands on another device, cannot vouch for the clock either.
|
|
@@ -42,8 +42,8 @@ module Rigor
|
|
|
42
42
|
# plugin's identity and the engine's (key slots), so a project whose plugins claim template globs is served
|
|
43
43
|
# too. The ADR-87 probe misses on one: the plain slot's key carries the `template-units` slot.
|
|
44
44
|
#
|
|
45
|
-
# `explain:` is always false: the session
|
|
46
|
-
# `--explain`.
|
|
45
|
+
# `explain:` is always false: the session writes no slot for an `--explain` run (#1533), and the CLI declines
|
|
46
|
+
# the probe under `--explain`.
|
|
47
47
|
module IncrementalRunSlot
|
|
48
48
|
module_function
|
|
49
49
|
|
|
@@ -84,7 +84,9 @@ module Rigor
|
|
|
84
84
|
return nil if base.nil?
|
|
85
85
|
|
|
86
86
|
roots = RunCacheKey.config_entry("incremental.roots", normalize_roots(target.roots).sort.join("\n"))
|
|
87
|
-
|
|
87
|
+
# Issue #1814 — the opted-in gems' `(name, version, mode)` rows travel with the key, as they do in the plain
|
|
88
|
+
# probe's: the gem's walked source reaches every file's answer and no analysed file records it.
|
|
89
|
+
Cache::Descriptor.new(gems: base.gems, dependencies: base.dependencies, configs: base.configs + [roots])
|
|
88
90
|
end
|
|
89
91
|
|
|
90
92
|
# The roots as absolute paths, which the KEY holds sorted: `lib`, `./lib` and `lib/` are one root there. A
|
|
@@ -113,7 +115,7 @@ module Rigor
|
|
|
113
115
|
def serve(configuration:, cache_root:, paths:)
|
|
114
116
|
return nil if configuration.effects_enabled?
|
|
115
117
|
|
|
116
|
-
files = PathExpansion.ruby_files(paths, configuration.exclude_patterns)
|
|
118
|
+
files = PathExpansion.ruby_files(paths, configuration.exclude_patterns, base: configuration.exclude_base)
|
|
117
119
|
slot_key = key(configuration: configuration, target: Target.new(files: files, roots: paths))
|
|
118
120
|
return nil if slot_key.nil?
|
|
119
121
|
|
|
@@ -38,7 +38,7 @@ module Rigor
|
|
|
38
38
|
# gate) can report what was re-analyzed versus served from cache. `added` / `removed` carry the
|
|
39
39
|
# structural delta so {#run_incremental} (ADR-87 WD3) can recognise a zero-change recheck and skip the
|
|
40
40
|
# unconditional snapshot rewrite.
|
|
41
|
-
Recheck = Data.define(:diagnostics, :changed, :added, :removed, :affected, :reused) do
|
|
41
|
+
Recheck = Data.define(:diagnostics, :changed, :added, :removed, :affected, :reused, :templates) do
|
|
42
42
|
# A recheck that changed, added, and removed nothing — the session state is byte-equivalent to the
|
|
43
43
|
# snapshot that was restored, so persisting it again would only rewrite identical bytes.
|
|
44
44
|
def no_change?
|
|
@@ -68,9 +68,14 @@ module Rigor
|
|
|
68
68
|
# `--verify-incremental` gate use) keeps the sequential recording path bit-for-bit unchanged. The fork
|
|
69
69
|
# pool records each worker's cross-file reads and marshals them back (PoolCoordinator), so the
|
|
70
70
|
# dependency graph a pooled recheck rebuilds equals the sequential one.
|
|
71
|
-
def initialize(configuration:, paths: nil, environment: nil, cache_store: nil, plugin_requirer: nil,
|
|
72
|
-
workers: 0, buffer: nil)
|
|
71
|
+
def initialize(configuration:, paths: nil, environment: nil, cache_store: nil, plugin_requirer: nil, # rubocop:disable Metrics/ParameterLists,Metrics/AbcSize,Metrics/MethodLength
|
|
72
|
+
workers: 0, buffer: nil, explain: false, on_file_indexed: nil)
|
|
73
73
|
@configuration = configuration
|
|
74
|
+
# ADR-113 WD2 — `rigor lens`: {#force_targets=} names files a {#recheck} analyses whether or not they changed,
|
|
75
|
+
# in this process, so `on_file_indexed` (see {Runner}) sees their seeded index. Neither moves a diagnostic:
|
|
76
|
+
# a forced file re-analyses under the same seeds its cached rows were computed with.
|
|
77
|
+
@force_targets = Set.new
|
|
78
|
+
@on_file_indexed = on_file_indexed
|
|
74
79
|
@paths = paths
|
|
75
80
|
# Editor mode option B (#146) — the in-flight buffer, threaded into every internal Runner so the
|
|
76
81
|
# pre-passes and the closure re-analysis read the editor's bytes at the logical path. A session
|
|
@@ -81,6 +86,7 @@ module Rigor
|
|
|
81
86
|
@cache_store = cache_store
|
|
82
87
|
@plugin_requirer = plugin_requirer
|
|
83
88
|
@workers = workers
|
|
89
|
+
@explain = explain # #1533: threaded into every Runner; keys the snapshot, and no slot is written
|
|
84
90
|
# ADR-85 WD2 — per-file discovery seed bundles keyed by logical path. A cold baseline builds them; a
|
|
85
91
|
# warm recheck folds them (re-walking only changed files) and refreshes the set. Ride the snapshot.
|
|
86
92
|
@seed_bundles = {}
|
|
@@ -122,12 +128,31 @@ module Rigor
|
|
|
122
128
|
# banner + `--cache-stats`) reads after `#run_incremental`. `@last_runner` is the analysis runner the
|
|
123
129
|
# post-hoc fingerprint reads from; `@plugin_fact_reusable` is the computed decision object.
|
|
124
130
|
@plugin_fact_digest = nil
|
|
125
|
-
@
|
|
131
|
+
@opaque_reasons = {}.freeze
|
|
132
|
+
@fact_store_unreadable = false
|
|
126
133
|
@fact_surface_invalidated = false
|
|
127
134
|
@last_runner = nil
|
|
128
135
|
@plugin_fact_reusable = nil
|
|
129
136
|
end
|
|
130
137
|
|
|
138
|
+
# ADR-113 WD2 — the paths (as {#project_files} spells them) every later {#recheck} analyses in this process.
|
|
139
|
+
def force_targets=(paths)
|
|
140
|
+
@force_targets = paths.to_set
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The project files a run of this session analyses, as the runner expands the configured paths.
|
|
144
|
+
def project_files
|
|
145
|
+
current_files
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The per-file discovery bundles of the last run, keyed by path (ADR-85 WD2).
|
|
149
|
+
attr_reader :seed_bundles
|
|
150
|
+
|
|
151
|
+
# The project files `path`'s analysis read from at the last run that analysed it (ADR-46 slice 1).
|
|
152
|
+
def dependency_sources(path)
|
|
153
|
+
@sources.fetch(path, Set.new)
|
|
154
|
+
end
|
|
155
|
+
|
|
131
156
|
# The project files analyzed at the last baseline / recheck — the set a verify pass partitions and the
|
|
132
157
|
# merge subtracts the affected closure from.
|
|
133
158
|
def analyzed_files
|
|
@@ -206,13 +231,13 @@ module Rigor
|
|
|
206
231
|
fresh_params = fresh_param_table(current, changed, added, removed)
|
|
207
232
|
param_files, param_pairs = param_seed_invalidation(fresh_params)
|
|
208
233
|
affected = affected_closure(changed, added, removed, param_files, param_pairs)
|
|
209
|
-
analyze_set = affected
|
|
210
|
-
reused = (current & previous) -
|
|
234
|
+
forced, analyze_set = forced_analyze_set(affected, current)
|
|
235
|
+
reused = (current & previous) - analyze_set.to_a
|
|
211
236
|
# The freshly collected table is handed to the runner so the run seeds from the SAME table the diff
|
|
212
237
|
# was decided on (and the collector runs once per recheck, not twice). The reused files' cached rows
|
|
213
238
|
# go in too, so the runner lists them in the full run's file order (#1524).
|
|
214
239
|
runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
|
|
215
|
-
param_inferred_types: fresh_params,
|
|
240
|
+
param_inferred_types: fresh_params, **forced_target_options(forced),
|
|
216
241
|
restored_run_level_rows: @run_level_rows,
|
|
217
242
|
served_per_file_diagnostics: @cache.slice(*reused))
|
|
218
243
|
merged = run_runner(runner).diagnostics
|
|
@@ -220,7 +245,8 @@ module Rigor
|
|
|
220
245
|
absorb(runner, current, analyze_set, removed)
|
|
221
246
|
@param_table = fresh_params
|
|
222
247
|
Recheck.new(diagnostics: merged, changed: changed.to_set, added: added.to_set,
|
|
223
|
-
removed: removed.to_set, affected: affected, reused: reused.to_set
|
|
248
|
+
removed: removed.to_set, affected: affected, reused: reused.to_set,
|
|
249
|
+
templates: runner.template_unit_paths.size)
|
|
224
250
|
end
|
|
225
251
|
|
|
226
252
|
# Editor mode option B (#146) — a whole-project recheck with the editor's buffer substituted for one
|
|
@@ -480,6 +506,7 @@ module Rigor
|
|
|
480
506
|
# to. Every soundness gate below — the fingerprint, the ADR-88 fact surface — is unchanged, so the
|
|
481
507
|
# decision to reuse is made identically whether or not the result is saved.
|
|
482
508
|
def run_incremental(snapshot:, fingerprint:, persist: true)
|
|
509
|
+
@last_recheck = nil # #1526 — set only by a warm recheck, see {#last_recheck}
|
|
483
510
|
# ADR-87 WD1 — install the per-run digest table + recording instant + strict flag for the whole
|
|
484
511
|
# invocation so change-detection's stat-then-digest freshness (`#pack_digest` / `#stat_fresh?`) honours
|
|
485
512
|
# `cache.validation: digest` (and `RIGOR_STRICT_VALIDATION`, which the env-only path already sees) and
|
|
@@ -508,7 +535,7 @@ module Rigor
|
|
|
508
535
|
adopt_plugin_fact_fingerprint
|
|
509
536
|
fact_reusable = @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
|
|
510
537
|
if fact_reusable && effects_reusable && !source_rbs_unverified?(result)
|
|
511
|
-
diagnostics = result
|
|
538
|
+
diagnostics = keep_recheck(result)
|
|
512
539
|
warm = true
|
|
513
540
|
# ADR-87 WD3 — a warm recheck that changed nothing leaves the session state byte-equivalent to the
|
|
514
541
|
# snapshot it restored, so skip the unconditional rewrite (209ms + 2 MB on gitlab per null recheck).
|
|
@@ -539,9 +566,36 @@ module Rigor
|
|
|
539
566
|
|
|
540
567
|
# ADR-88 WD1 — reporting hooks the CLI reads after {#run_incremental}. `fact_surface_invalidated?` is true
|
|
541
568
|
# when a valid snapshot was dropped for a fact-surface reason (a plugin sig/catalog edit, or an opaque
|
|
542
|
-
# plugin) rather than a cold miss. `
|
|
543
|
-
#
|
|
544
|
-
attr_reader :
|
|
569
|
+
# plugin) rather than a cold miss. `opaque_reasons` maps each plugin that forces a full run every invocation
|
|
570
|
+
# to its {PluginFactFingerprint::OpaqueReason}.
|
|
571
|
+
attr_reader :opaque_reasons
|
|
572
|
+
|
|
573
|
+
# #1526 — the {Recheck} of the last {#run_incremental}, or nil unless that run answered warm. A baseline
|
|
574
|
+
# (cold, or a re-baseline after a fact-surface, effects or source-RBS reason) has no recheck to report.
|
|
575
|
+
# The diagnostics are not kept: a long-lived session (the LSP) would otherwise retain every merged list.
|
|
576
|
+
attr_reader :last_recheck
|
|
577
|
+
|
|
578
|
+
# Records the warm recheck's sets for {#last_recheck} and hands back its diagnostics.
|
|
579
|
+
def keep_recheck(result)
|
|
580
|
+
@last_recheck = result.with(diagnostics: [])
|
|
581
|
+
result.diagnostics
|
|
582
|
+
end
|
|
583
|
+
private :keep_recheck
|
|
584
|
+
|
|
585
|
+
# The opaque plugin ids, sorted (the keys of {#opaque_reasons}).
|
|
586
|
+
def opaque_plugin_ids
|
|
587
|
+
@opaque_reasons.keys
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# True when the fact store could not be read, so no single plugin is named (see {PluginFactFingerprint::Result}).
|
|
591
|
+
def fact_store_unreadable?
|
|
592
|
+
@fact_store_unreadable
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# True when the fact surface forces a full run: some plugin is opaque, or the fact store is unreadable.
|
|
596
|
+
def opaque_fact_surface?
|
|
597
|
+
!@opaque_reasons.empty? || @fact_store_unreadable
|
|
598
|
+
end
|
|
545
599
|
|
|
546
600
|
def fact_surface_invalidated?
|
|
547
601
|
@fact_surface_invalidated
|
|
@@ -566,7 +620,8 @@ module Rigor
|
|
|
566
620
|
registry = prepared_plugin_registry
|
|
567
621
|
fact = compute_plugin_fact_fingerprint(registry)
|
|
568
622
|
@plugin_fact_digest = fact.digest
|
|
569
|
-
@
|
|
623
|
+
@opaque_reasons = fact.opaque_reasons
|
|
624
|
+
@fact_store_unreadable = fact.fact_store_unreadable
|
|
570
625
|
@plugin_fact_reusable = fact
|
|
571
626
|
@seed_bundles = source_rbs_gate.verify(registry, @seed_bundles)
|
|
572
627
|
end
|
|
@@ -596,10 +651,10 @@ module Rigor
|
|
|
596
651
|
|
|
597
652
|
# ADR-88 WD1 — the plugin fact-surface fingerprint for this invocation. For a SEQUENTIAL run it is read
|
|
598
653
|
# post-hoc from the analysis runner's already-prepared registry (no second `#prepare`, producers already
|
|
599
|
-
# validated during analysis). A POOLED run's main process
|
|
600
|
-
# published facts — there it falls back to the always-sequential probe. Both
|
|
601
|
-
# digest for a given fact surface, so the reuse decision is worker-count-independent
|
|
602
|
-
# asserts this).
|
|
654
|
+
# validated during analysis). A POOLED run's main process does not run `#prepare` on it (its workers do),
|
|
655
|
+
# so its registry carries no published facts — there it falls back to the always-sequential probe. Both
|
|
656
|
+
# paths compute the identical digest for a given fact surface, so the reuse decision is worker-count-independent
|
|
657
|
+
# (the parity spec asserts this).
|
|
603
658
|
def compute_plugin_fact_fingerprint(registry = prepared_plugin_registry)
|
|
604
659
|
PluginFactFingerprint.from_registry(registry)
|
|
605
660
|
end
|
|
@@ -710,7 +765,8 @@ module Rigor
|
|
|
710
765
|
return nil if fingerprint.nil? || @cache_store.nil? || @cache_store.read_only? || !@buffer.nil?
|
|
711
766
|
# Known before the run: none of these can be written afterwards, so the stamp and the recomputed
|
|
712
767
|
# fingerprint would be spent for nothing.
|
|
713
|
-
|
|
768
|
+
# `WriteGuard.start` would also refuse (it recomputes the fingerprint without `explain:`); say so here.
|
|
769
|
+
return nil unless @workers.zero? && !@configuration.effects_enabled? && !@explain
|
|
714
770
|
|
|
715
771
|
IncrementalRunSlot::WriteGuard.start(
|
|
716
772
|
configuration: @configuration, roots: @paths || @configuration.paths, cache_root: @cache_store.root,
|
|
@@ -724,7 +780,7 @@ module Rigor
|
|
|
724
780
|
# cold, not the warm run the probe stands in for.
|
|
725
781
|
def run_slot_writable?
|
|
726
782
|
!@cache_store.nil? && !@cache_store.read_only? && @buffer.nil? && @workers.zero? &&
|
|
727
|
-
!@configuration.effects_enabled? &&
|
|
783
|
+
!@configuration.effects_enabled? && !@explain && !opaque_fact_surface? && !@last_runner.nil?
|
|
728
784
|
end
|
|
729
785
|
|
|
730
786
|
def start_chain(rows)
|
|
@@ -1090,12 +1146,24 @@ module Rigor
|
|
|
1090
1146
|
changed + removed, @constant_decls, new_constant_decls
|
|
1091
1147
|
)
|
|
1092
1148
|
keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
|
|
1093
|
-
keys.concat(moved_classes
|
|
1149
|
+
keys.concat(class_move_keys(moved_classes))
|
|
1094
1150
|
keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
|
|
1095
1151
|
Incremental.negative_closure(keys, @negative_dependents) | refinement_affected(changed + removed, summary) |
|
|
1096
1152
|
global_write_reporters
|
|
1097
1153
|
end
|
|
1098
1154
|
|
|
1155
|
+
# The negative keys a class-declaration diff fires: `class:<last segment>` for a class name, and (issue #1820)
|
|
1156
|
+
# `subclass_of:<last segment of the superclass>` for a `[class, superclass]` pair appearing or vanishing. Any
|
|
1157
|
+
# class declaration moving may move the set of classes whose superclass the tables do not know, which
|
|
1158
|
+
# `subclass_of:*` stands for.
|
|
1159
|
+
def class_move_keys(moved)
|
|
1160
|
+
names, edges = moved.partition { |entry| entry.is_a?(String) }
|
|
1161
|
+
keys = names.map { |klass| "class:#{klass.split('::').last}" }
|
|
1162
|
+
keys.concat(edges.map { |(_child, parent)| "subclass_of:#{parent.split('::').last}" })
|
|
1163
|
+
keys << "subclass_of:*" unless moved.empty?
|
|
1164
|
+
keys
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1099
1167
|
# Issue #1120 — the consumers whose `call.undefined-method` answer read the refinement table for a method
|
|
1100
1168
|
# name whose refinements moved in this edit. The before-state is each file's seed bundle (the per-file
|
|
1101
1169
|
# table the last run recorded), the after-state the scan's per-file tables. A refine body is not a class's
|
|
@@ -1115,9 +1183,10 @@ module Rigor
|
|
|
1115
1183
|
return {} if index.nil?
|
|
1116
1184
|
|
|
1117
1185
|
result = Hash.new { |hash, key| hash[key] = Set.new }
|
|
1118
|
-
index[:class_sources].each do |
|
|
1119
|
-
files.each { |file| result[file] << class_name }
|
|
1186
|
+
[index[:class_sources], index[:singleton_class_sources] || {}].each do |sources|
|
|
1187
|
+
sources.each { |class_name, files| files.each { |file| result[file] << class_name } }
|
|
1120
1188
|
end
|
|
1189
|
+
Incremental.add_superclass_edges(result, index[:class_sources], index[:superclasses])
|
|
1121
1190
|
result.transform_values(&:freeze).freeze
|
|
1122
1191
|
end
|
|
1123
1192
|
|
|
@@ -1256,10 +1325,21 @@ module Rigor
|
|
|
1256
1325
|
Runner.new(
|
|
1257
1326
|
configuration: @configuration, cache_store: @cache_store, environment: @environment,
|
|
1258
1327
|
plugin_requirer: @plugin_requirer, seed_bundles: @seed_bundles, collect_seed_bundles: true,
|
|
1259
|
-
workers: @workers, buffer: @buffer, **
|
|
1328
|
+
workers: @workers, buffer: @buffer, explain: @explain, on_file_indexed: @on_file_indexed, **
|
|
1260
1329
|
)
|
|
1261
1330
|
end
|
|
1262
1331
|
|
|
1332
|
+
# The forced targets present in `current`, and the recheck's analyse set with them joined to the closure.
|
|
1333
|
+
def forced_analyze_set(affected, current)
|
|
1334
|
+
forced = @force_targets & current.to_set
|
|
1335
|
+
[forced, (affected | forced) & current]
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1338
|
+
# A recheck that forces targets analyses in this process, so the index callback reaches them.
|
|
1339
|
+
def forced_target_options(forced)
|
|
1340
|
+
forced.empty? ? {} : { workers: 0 }
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1263
1343
|
# Run the runner over the session's explicit paths (or, when none were given, the configuration's
|
|
1264
1344
|
# `paths:` via `Runner#run`'s default).
|
|
1265
1345
|
def run_runner(runner)
|
|
@@ -14,10 +14,10 @@ module Rigor
|
|
|
14
14
|
module_function
|
|
15
15
|
|
|
16
16
|
# The `.rb` files an ordinary (non-buffer) check over `paths` would analyze.
|
|
17
|
-
def ruby_files(paths, exclude_patterns)
|
|
17
|
+
def ruby_files(paths, exclude_patterns, base: Dir.pwd)
|
|
18
18
|
Array(paths).flat_map do |path|
|
|
19
19
|
if File.directory?(path)
|
|
20
|
-
directory_files(path, exclude_patterns)
|
|
20
|
+
directory_files(path, exclude_patterns, base: base)
|
|
21
21
|
elsif File.file?(path) && path.end_with?(".rb")
|
|
22
22
|
[path]
|
|
23
23
|
else
|
|
@@ -26,16 +26,72 @@ module Rigor
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def directory_files(dir, exclude_patterns)
|
|
30
|
-
reject_excluded(Dir.glob(File.join(dir, RUBY_GLOB)), exclude_patterns)
|
|
29
|
+
def directory_files(dir, exclude_patterns, base: Dir.pwd)
|
|
30
|
+
reject_excluded(Dir.glob(File.join(dir, RUBY_GLOB)), exclude_patterns, base: base)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# `exclude_patterns` are matched with `File.fnmatch?` WITHOUT `FNM_PATHNAME`, so `**`/`*` span separators
|
|
34
34
|
# (the patterns behave like substring globs) — the exact semantics the Runner's directory glob applies.
|
|
35
|
-
|
|
35
|
+
#
|
|
36
|
+
# #1576 — a RELATIVE pattern (a leading `./` is ignored) is matched against the file's path relative to
|
|
37
|
+
# `base` (the config file's directory, or the working directory with no config), so the same file is
|
|
38
|
+
# excluded whether `paths:` (absolute) or an argument (relative) named it.
|
|
39
|
+
#
|
|
40
|
+
# Two spellings are tried. The LOGICAL one (the path as given, against the logical base) counts only when
|
|
41
|
+
# the file lies inside the base; it is what lets a symlinked `lib` match `lib/gen/**`. The REALPATH one
|
|
42
|
+
# (realpath of the file against realpath of the base) is the only one used for a file outside the base, and
|
|
43
|
+
# it also catches a base or file spelled through a different symlink root (macOS `/tmp` vs `/private/tmp`).
|
|
44
|
+
# Neither shows a directory name above the base: a file outside it comes out as `../…`, so `**/tmp/**`
|
|
45
|
+
# cannot exclude a project merely located under a `tmp/`, while `**/vendor/bundle/**` still reaches
|
|
46
|
+
# `../other/vendor/bundle/x.rb`. The pattern is also tried against `/<relative>`, because without
|
|
47
|
+
# `FNM_PATHNAME` the unanchored `**/tmp/**` needs a leading `/` to match a top-level `tmp/x.rb` (and `*`
|
|
48
|
+
# never matches a leading `.`, as in `../other`). A pattern starting with `.` therefore also matches files
|
|
49
|
+
# outside the base. An ABSOLUTE pattern is matched only against the absolute path (logical, then real).
|
|
50
|
+
def reject_excluded(file_list, exclude_patterns, base: Dir.pwd)
|
|
36
51
|
return file_list if exclude_patterns.empty?
|
|
37
52
|
|
|
38
|
-
|
|
53
|
+
absolute_patterns, relative_patterns = exclude_patterns.partition { |pattern| pattern.start_with?("/") }
|
|
54
|
+
relative_patterns = relative_patterns.map { |pattern| pattern.sub(%r{\A(?:\./+)+}, "") }
|
|
55
|
+
logical_base = File.absolute_path(base)
|
|
56
|
+
context = { cwd: Dir.pwd, base: logical_base, real_base: real_directory(logical_base), real_dirs: {} }
|
|
57
|
+
file_list.reject { |path| excluded?(path, absolute_patterns, relative_patterns, context) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def excluded?(path, absolute_patterns, relative_patterns, context)
|
|
61
|
+
logical = File.absolute_path(path, context.fetch(:cwd))
|
|
62
|
+
base = context.fetch(:base)
|
|
63
|
+
inside = logical.start_with?("#{base}/")
|
|
64
|
+
return true if inside && matches_relative?(relative_patterns, logical.byteslice((base.bytesize + 1)..))
|
|
65
|
+
return true if absolute_patterns.any? { |pattern| File.fnmatch?(pattern, logical) }
|
|
66
|
+
|
|
67
|
+
real = real_file(logical, context.fetch(:real_dirs))
|
|
68
|
+
return false if real == logical && inside && context.fetch(:real_base) == base
|
|
69
|
+
|
|
70
|
+
matches_relative?(relative_patterns, relative_path(real, context.fetch(:real_base))) ||
|
|
71
|
+
(real != logical && absolute_patterns.any? { |pattern| File.fnmatch?(pattern, real) })
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def relative_path(absolute, base)
|
|
75
|
+
return absolute.byteslice((base.bytesize + 1)..) if absolute.start_with?("#{base}/")
|
|
76
|
+
|
|
77
|
+
Pathname.new(absolute).relative_path_from(Pathname.new(base)).to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def matches_relative?(patterns, relative)
|
|
81
|
+
candidates = [relative, "/#{relative}"]
|
|
82
|
+
patterns.any? { |pattern| candidates.any? { |candidate| File.fnmatch?(pattern, candidate) } }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def real_directory(dir)
|
|
86
|
+
File.realpath(dir)
|
|
87
|
+
rescue SystemCallError
|
|
88
|
+
File.absolute_path(dir)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def real_file(absolute, real_dirs)
|
|
92
|
+
dir = File.dirname(absolute)
|
|
93
|
+
real_dir = real_dirs[dir] ||= real_directory(dir)
|
|
94
|
+
File.join(real_dir, File.basename(absolute))
|
|
39
95
|
end
|
|
40
96
|
end
|
|
41
97
|
end
|