rigortype 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "incremental_sync"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module LanguageServer
|
|
5
7
|
# Per-session virtual file table. The LSP server maintains the canonical view of every open buffer here;
|
|
6
8
|
# analysis (slice 4+) reads from this table instead of disk so in-flight edits are reflected immediately.
|
|
7
9
|
#
|
|
8
|
-
# Keyed by `DocumentUri` (LSP `file://...` URIs).
|
|
9
|
-
# `TextDocumentSyncKind::
|
|
10
|
-
#
|
|
10
|
+
# Keyed by `DocumentUri` (LSP `file://...` URIs). Sync is INCREMENTAL (LSP
|
|
11
|
+
# `TextDocumentSyncKind::Incremental = 2`): a `didChange` carries range edits which {#apply_changes} splices
|
|
12
|
+
# into the held text through {IncrementalSync}. The full-text form ({#change}, and a `contentChanges` entry
|
|
13
|
+
# with no `range`) stays supported — it is still legal under incremental sync and is what `didOpen` and a
|
|
14
|
+
# client-side resync send.
|
|
11
15
|
class BufferTable
|
|
12
16
|
# @!attribute uri [String] the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
|
|
13
17
|
# @!attribute bytes [String] the current full text of the buffer.
|
|
@@ -16,23 +20,59 @@ module Rigor
|
|
|
16
20
|
|
|
17
21
|
def initialize
|
|
18
22
|
@entries = {}
|
|
23
|
+
@desynchronized = {}
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
# Records a `textDocument/didOpen` event. Replaces any existing entry (LSP clients may re-open a
|
|
22
|
-
# previously closed URI; the new version is authoritative)
|
|
27
|
+
# previously closed URI; the new version is authoritative) and clears any desynchronised mark — the
|
|
28
|
+
# payload carries the client's full text, so the two views agree again.
|
|
23
29
|
def open(uri:, bytes:, version:)
|
|
30
|
+
@desynchronized.delete(uri)
|
|
24
31
|
@entries[uri] = Entry.new(uri: uri, bytes: bytes, version: version)
|
|
25
32
|
end
|
|
26
33
|
|
|
27
|
-
# Records a `textDocument/didChange
|
|
28
|
-
#
|
|
34
|
+
# Records a full-text `textDocument/didChange`. The full new buffer text replaces the entry. If the
|
|
35
|
+
# client sends a `didChange` for a URI that was never opened (spec violation), the entry is still
|
|
29
36
|
# created — defensive.
|
|
30
37
|
def change(uri:, bytes:, version:)
|
|
38
|
+
@desynchronized.delete(uri)
|
|
31
39
|
@entries[uri] = Entry.new(uri: uri, bytes: bytes, version: version)
|
|
32
40
|
end
|
|
33
41
|
|
|
42
|
+
# Applies a `textDocument/didChange` payload under INCREMENTAL sync. Each entry of `changes` is applied
|
|
43
|
+
# in order against the result of the previous one, per the LSP contract.
|
|
44
|
+
#
|
|
45
|
+
# On success the entry is replaced with the spliced text at the new version. On failure — a malformed
|
|
46
|
+
# change, or a range edit for a URI with no held buffer — the held text is left EXACTLY as it was and the
|
|
47
|
+
# URI is marked desynchronised: the server's view and the editor's view have diverged, and every position
|
|
48
|
+
# computed from the stale text would be wrong. Consumers check {#desynchronized?} and decline to answer
|
|
49
|
+
# rather than answering wrongly; the mark clears on the next `didOpen` or full-text change.
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean] true when the changes were applied.
|
|
52
|
+
def apply_changes(uri:, changes:, version:)
|
|
53
|
+
text = IncrementalSync.apply_all(@entries[uri]&.bytes, changes)
|
|
54
|
+
@desynchronized.delete(uri)
|
|
55
|
+
@entries[uri] = Entry.new(uri: uri, bytes: text, version: version)
|
|
56
|
+
true
|
|
57
|
+
rescue IncrementalSync::UnappliableChange => e
|
|
58
|
+
@desynchronized[uri] = e.message
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Boolean] true when the last `didChange` for `uri` could not be applied, so the held text no
|
|
63
|
+
# longer matches the editor's.
|
|
64
|
+
def desynchronized?(uri)
|
|
65
|
+
@desynchronized.key?(uri)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [String, nil] why `uri` is desynchronised, for the log line; nil when it is in sync.
|
|
69
|
+
def desynchronization_reason(uri)
|
|
70
|
+
@desynchronized[uri]
|
|
71
|
+
end
|
|
72
|
+
|
|
34
73
|
# Records a `textDocument/didClose` event. The entry is removed. Subsequent reads via `#[]` return nil.
|
|
35
74
|
def close(uri:)
|
|
75
|
+
@desynchronized.delete(uri)
|
|
36
76
|
@entries.delete(uri)
|
|
37
77
|
end
|
|
38
78
|
|
|
@@ -8,6 +8,7 @@ require_relative "../environment"
|
|
|
8
8
|
require_relative "../reflection"
|
|
9
9
|
require_relative "../scope"
|
|
10
10
|
require_relative "../source/node_locator"
|
|
11
|
+
require_relative "../source/node_children"
|
|
11
12
|
require_relative "../inference/scope_indexer"
|
|
12
13
|
require_relative "../type/nominal"
|
|
13
14
|
require_relative "../type/singleton"
|
|
@@ -187,7 +188,7 @@ module Rigor
|
|
|
187
188
|
n.location.start_offset <= symbol_offset && symbol_offset <= n.location.end_offset
|
|
188
189
|
result = n
|
|
189
190
|
end
|
|
190
|
-
n.
|
|
191
|
+
n.rigor_each_child(&walk)
|
|
191
192
|
end
|
|
192
193
|
walk.call(root)
|
|
193
194
|
result
|
|
@@ -80,6 +80,10 @@ module Rigor
|
|
|
80
80
|
# The buffer may have been closed during the debounce window — drop the publish; the empty
|
|
81
81
|
# notification from didClose already cleared the markers.
|
|
82
82
|
return if entry.nil?
|
|
83
|
+
# An incremental change the table could not apply: the held text no longer matches the editor's, so
|
|
84
|
+
# every span we could compute from it would be misplaced. Clear the markers and stay silent until a
|
|
85
|
+
# full-text change or a re-open re-establishes the buffer.
|
|
86
|
+
return notify(uri, []) if @buffer_table.desynchronized?(uri)
|
|
83
87
|
|
|
84
88
|
diagnostics = run_analysis(path: path, bytes: entry.bytes)
|
|
85
89
|
notify(uri, diagnostics)
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
6
|
require_relative "buffer_resolution"
|
|
7
|
+
require_relative "../source/node_children"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module LanguageServer
|
|
@@ -67,7 +68,7 @@ module Rigor
|
|
|
67
68
|
when Prism::DefNode
|
|
68
69
|
block.call(def_symbol(node, in_namespace: in_namespace))
|
|
69
70
|
else
|
|
70
|
-
node.
|
|
71
|
+
node.rigor_each_child do |child|
|
|
71
72
|
each_decl(child, in_namespace: in_namespace, &block)
|
|
72
73
|
end
|
|
73
74
|
end
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
6
|
require_relative "buffer_resolution"
|
|
7
|
+
require_relative "../source/node_children"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module LanguageServer
|
|
@@ -48,7 +49,7 @@ module Rigor
|
|
|
48
49
|
Prism::BlockNode
|
|
49
50
|
add_range(node, ranges)
|
|
50
51
|
end
|
|
51
|
-
node.
|
|
52
|
+
node.rigor_each_child { |child| walk(child, ranges) }
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def add_range(node, ranges)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module LanguageServer
|
|
7
|
+
# Applies LSP `TextDocumentContentChangeEvent`s to a held buffer under `TextDocumentSyncKind::Incremental`.
|
|
8
|
+
#
|
|
9
|
+
# ## Why the offsets need care
|
|
10
|
+
#
|
|
11
|
+
# An LSP `Position#character` counts **UTF-16 code units** (`positionEncoding: "utf-16"`, the protocol
|
|
12
|
+
# default and the only encoding every client supports), while a Ruby String indexes by **codepoint**. The
|
|
13
|
+
# two agree for every character in the Basic Multilingual Plane — all of ASCII, Latin-1, Greek, Cyrillic,
|
|
14
|
+
# kana and common kanji — and disagree above U+FFFF, where an emoji or a CJK-extension ideograph is ONE
|
|
15
|
+
# Ruby character and TWO UTF-16 code units. Reading one as the other shifts every subsequent edit on the
|
|
16
|
+
# line, and a shifted edit desynchronises the server's text from what the editor shows for the rest of the
|
|
17
|
+
# session: every diagnostic after that point lands on the wrong span, silently.
|
|
18
|
+
#
|
|
19
|
+
# So the conversion is explicit. {.utf16_offset_to_index} walks the line one character at a time, charging
|
|
20
|
+
# 2 code units for a codepoint above U+FFFF and 1 for everything else, and stops when the requested count
|
|
21
|
+
# is spent. An all-ASCII line short-circuits the walk, since there the offset IS the index — that is the
|
|
22
|
+
# keystroke path, and `String#ascii_only?` answers it from the cached coderange.
|
|
23
|
+
#
|
|
24
|
+
# ## Failure is a resync, never a guess
|
|
25
|
+
#
|
|
26
|
+
# A change whose shape cannot be applied confidently raises {UnappliableChange} rather than producing a
|
|
27
|
+
# best-effort buffer: {BufferTable#apply_changes} then keeps the last known-good text and marks the URI
|
|
28
|
+
# desynchronised, which suppresses diagnostics until a full-text change or a re-open re-establishes the
|
|
29
|
+
# buffer. A stale-but-flagged buffer is recoverable; a silently wrong one is not.
|
|
30
|
+
module IncrementalSync
|
|
31
|
+
# Raised when a `contentChanges` entry cannot be applied to the held text: a malformed payload, a
|
|
32
|
+
# position that is not a non-negative Integer pair, or a range edit against a buffer the server never
|
|
33
|
+
# received a `didOpen` / full-text change for.
|
|
34
|
+
class UnappliableChange < StandardError; end
|
|
35
|
+
|
|
36
|
+
# LSP considers a line delimited by `\n`, `\r\n`, or a lone `\r`. Ruby's own line splitting only knows
|
|
37
|
+
# `\n`, so the scan is explicit.
|
|
38
|
+
LINE_TERMINATOR = /\r\n|\n|\r/
|
|
39
|
+
|
|
40
|
+
# UTF-8 encodes exactly the codepoints above the BMP — the ones UTF-16 must encode as a surrogate pair —
|
|
41
|
+
# in four bytes. So a character's UTF-8 byte length answers "one code unit or two?" without decoding it.
|
|
42
|
+
SURROGATE_PAIR_BYTES = 4
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# Applies every change in order, each against the result of the previous — the LSP contract for a
|
|
47
|
+
# multi-change `didChange` notification.
|
|
48
|
+
#
|
|
49
|
+
# @param text [String, nil] the held buffer text; nil when no buffer is open for the URI.
|
|
50
|
+
# @param changes [Array<Hash>] the `contentChanges` array.
|
|
51
|
+
# @return [String] the new buffer text.
|
|
52
|
+
# @raise [UnappliableChange] if any change cannot be applied.
|
|
53
|
+
def apply_all(text, changes)
|
|
54
|
+
raise UnappliableChange, "contentChanges must be an Array, got #{changes.class}" unless changes.is_a?(Array)
|
|
55
|
+
|
|
56
|
+
changes.reduce(text) { |acc, change| apply(acc, change) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Applies one `TextDocumentContentChangeEvent`.
|
|
60
|
+
#
|
|
61
|
+
# Two shapes are legal under incremental sync. Without `range` the entry is the full new document text
|
|
62
|
+
# (clients fall back to it for a paste, an undo, or a file reload, and it stays legal under
|
|
63
|
+
# `Incremental`); with `range` it replaces the spanned text. `rangeLength` is the deprecated pre-3.16
|
|
64
|
+
# companion to `range` and is accepted-but-ignored: it is redundant with `range`, historically ambiguous
|
|
65
|
+
# about its units, and `range` is the authoritative field.
|
|
66
|
+
#
|
|
67
|
+
# @raise [UnappliableChange]
|
|
68
|
+
def apply(text, change)
|
|
69
|
+
raise UnappliableChange, "contentChanges entry must be a Hash, got #{change.class}" unless change.is_a?(Hash)
|
|
70
|
+
|
|
71
|
+
replacement = change[:text]
|
|
72
|
+
raise UnappliableChange, "contentChanges entry has no `text`" unless replacement.is_a?(String)
|
|
73
|
+
|
|
74
|
+
range = change[:range]
|
|
75
|
+
return replacement.dup if range.nil?
|
|
76
|
+
raise UnappliableChange, "`range` must be a Hash, got #{range.class}" unless range.is_a?(Hash)
|
|
77
|
+
raise UnappliableChange, "range edit for a URI with no open buffer" if text.nil?
|
|
78
|
+
|
|
79
|
+
splice(text, range, replacement)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Replaces the `range` span of `text` with `replacement`.
|
|
83
|
+
#
|
|
84
|
+
# Text that is not valid UTF-8 makes the line scan itself raise. That is not a buffer JSON transport can
|
|
85
|
+
# deliver, but if one appears there is no offset arithmetic to be confident about, so it becomes a
|
|
86
|
+
# resync like any other unappliable shape.
|
|
87
|
+
def splice(text, range, replacement)
|
|
88
|
+
spans = line_spans(text)
|
|
89
|
+
from = char_offset(text, spans, range[:start])
|
|
90
|
+
# A client that inverts the range — or an `end` rounded below `start` off a mid-surrogate
|
|
91
|
+
# position — would otherwise slice backwards; collapse to an insertion at `from` instead.
|
|
92
|
+
to = char_offset(text, spans, range[:end]).clamp(from, text.length)
|
|
93
|
+
"#{text[0, from]}#{replacement}#{text[to..]}"
|
|
94
|
+
rescue ArgumentError, Encoding::CompatibilityError => e
|
|
95
|
+
raise UnappliableChange, "held text is not valid UTF-8: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @return [Array<Array(Integer, Integer)>] one `[content_start, content_end]` pair per line, in Ruby
|
|
99
|
+
# character indices. `content_end` excludes the line terminator, so it doubles as the clamp target for
|
|
100
|
+
# an over-long `character`. A trailing terminator yields a final empty span — the virtual last line an
|
|
101
|
+
# editor puts the cursor on, and the anchor for an end-of-document insert.
|
|
102
|
+
def line_spans(text)
|
|
103
|
+
spans = []
|
|
104
|
+
scanner = StringScanner.new(text)
|
|
105
|
+
start = 0
|
|
106
|
+
while scanner.skip_until(LINE_TERMINATOR)
|
|
107
|
+
stop = scanner.charpos
|
|
108
|
+
spans << [start, stop - scanner.matched.length]
|
|
109
|
+
start = stop
|
|
110
|
+
end
|
|
111
|
+
spans << [start, text.length]
|
|
112
|
+
spans
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Converts an LSP `Position` into a Ruby character index into `text`.
|
|
116
|
+
#
|
|
117
|
+
# A `line` past the last line clamps to the end of the document rather than raising: clients do address
|
|
118
|
+
# the position one past the final line, and the clamp is what the protocol's own end-of-document
|
|
119
|
+
# convention implies.
|
|
120
|
+
def char_offset(text, spans, position)
|
|
121
|
+
line = position_field(position, :line)
|
|
122
|
+
character = position_field(position, :character)
|
|
123
|
+
return text.length if line >= spans.length
|
|
124
|
+
|
|
125
|
+
start, stop = spans[line]
|
|
126
|
+
start + utf16_offset_to_index(text[start...stop], character)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Converts a UTF-16 code-unit offset into `line` to a Ruby character index into the same line.
|
|
130
|
+
#
|
|
131
|
+
# Clamps an offset past the end of the line to the line length, per LSP's rule that a `character`
|
|
132
|
+
# greater than the line length defaults back to the line length.
|
|
133
|
+
def utf16_offset_to_index(line, units)
|
|
134
|
+
# Fast path: on an all-ASCII line one UTF-16 code unit is one Ruby character, so the offset is the
|
|
135
|
+
# index. This is the keystroke case, and `ascii_only?` reads the string's cached coderange.
|
|
136
|
+
return units.clamp(0, line.length) if line.ascii_only?
|
|
137
|
+
|
|
138
|
+
index = 0
|
|
139
|
+
remaining = units
|
|
140
|
+
line.each_char do |char|
|
|
141
|
+
break if remaining <= 0
|
|
142
|
+
|
|
143
|
+
remaining -= char.bytesize == SURROGATE_PAIR_BYTES ? 2 : 1
|
|
144
|
+
index += 1
|
|
145
|
+
end
|
|
146
|
+
# `remaining` below zero means the offset addressed the low half of a surrogate pair — a position no
|
|
147
|
+
# conforming client sends. Round DOWN to the character boundary; splitting the pair is not an option.
|
|
148
|
+
remaining.negative? ? index - 1 : index
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def position_field(position, key)
|
|
152
|
+
value = position.is_a?(Hash) ? position[key] : nil
|
|
153
|
+
return value if value.is_a?(Integer) && !value.negative?
|
|
154
|
+
|
|
155
|
+
raise UnappliableChange, "position `#{key}` must be a non-negative Integer, got #{value.inspect}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
6
|
require_relative "buffer_resolution"
|
|
7
|
+
require_relative "../source/node_children"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module LanguageServer
|
|
@@ -48,7 +49,7 @@ module Rigor
|
|
|
48
49
|
return chain unless node.location && offset_in?(node.location, offset)
|
|
49
50
|
|
|
50
51
|
chain << node
|
|
51
|
-
node.
|
|
52
|
+
node.rigor_each_child { |child| ancestor_chain(child, offset, chain) }
|
|
52
53
|
chain
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -21,8 +21,15 @@ module Rigor
|
|
|
21
21
|
ERROR_SERVER_NOT_INITIALIZED = -32_002
|
|
22
22
|
ERROR_INVALID_REQUEST_AFTER_SHUTDOWN = -32_600
|
|
23
23
|
|
|
24
|
-
# `TextDocumentSyncKind::
|
|
25
|
-
|
|
24
|
+
# `TextDocumentSyncKind::Incremental = 2`: `didChange` carries range edits, which `IncrementalSync`
|
|
25
|
+
# splices into the buffer the table already holds instead of re-sending — and re-parsing — the whole
|
|
26
|
+
# document on every keystroke. The full-text entry form (a `contentChanges` entry with no `range`) stays
|
|
27
|
+
# legal under this mode and is still handled.
|
|
28
|
+
TEXT_DOCUMENT_SYNC_INCREMENTAL = 2
|
|
29
|
+
|
|
30
|
+
# LSP `PositionEncodingKind`. UTF-16 is the protocol default and the encoding `IncrementalSync` does its
|
|
31
|
+
# offset arithmetic in; advertising it explicitly states the contract rather than leaving it implied.
|
|
32
|
+
POSITION_ENCODING_UTF16 = "utf-16"
|
|
26
33
|
|
|
27
34
|
# Methods callable BEFORE `initialize`. Per LSP spec § 3 only `initialize` and `exit` are allowed
|
|
28
35
|
# pre-initialization; every other request returns `ServerNotInitialized`. We also accept `shutdown` so a
|
|
@@ -151,9 +158,10 @@ module Rigor
|
|
|
151
158
|
|
|
152
159
|
def advertised_capabilities
|
|
153
160
|
caps = {
|
|
161
|
+
positionEncoding: POSITION_ENCODING_UTF16,
|
|
154
162
|
textDocumentSync: {
|
|
155
163
|
openClose: true,
|
|
156
|
-
change:
|
|
164
|
+
change: TEXT_DOCUMENT_SYNC_INCREMENTAL
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
caps[:hoverProvider] = true if @hover_provider
|
|
@@ -213,19 +221,21 @@ module Rigor
|
|
|
213
221
|
nil
|
|
214
222
|
end
|
|
215
223
|
|
|
216
|
-
# textDocument/didChange under
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
224
|
+
# textDocument/didChange under INCREMENTAL sync. Every `contentChanges` entry is applied in order, each
|
|
225
|
+
# against the result of the previous — an entry with a `range` splices that span, an entry without one is
|
|
226
|
+
# the full new document text. The application (and its UTF-16 offset arithmetic) lives in
|
|
227
|
+
# `IncrementalSync`; the table keeps the last known-good text and flags the URI when a change cannot be
|
|
228
|
+
# applied. Triggers `publishDiagnostics` either way: a desynchronised buffer publishes an EMPTY set,
|
|
229
|
+
# which clears the markers instead of leaving stale ones on screen.
|
|
220
230
|
def handle_did_change(params)
|
|
221
231
|
doc = params.fetch(:textDocument)
|
|
222
232
|
changes = params.fetch(:contentChanges)
|
|
223
233
|
return nil if changes.empty?
|
|
224
234
|
|
|
225
235
|
uri = doc.fetch(:uri)
|
|
226
|
-
@buffer_table.
|
|
236
|
+
@buffer_table.apply_changes(
|
|
227
237
|
uri: uri,
|
|
228
|
-
|
|
238
|
+
changes: changes,
|
|
229
239
|
version: doc.fetch(:version)
|
|
230
240
|
)
|
|
231
241
|
@publisher&.publish_for(uri)
|
|
@@ -8,6 +8,7 @@ require_relative "../environment"
|
|
|
8
8
|
require_relative "../reflection"
|
|
9
9
|
require_relative "../scope"
|
|
10
10
|
require_relative "../source/node_locator"
|
|
11
|
+
require_relative "../source/node_children"
|
|
11
12
|
require_relative "../inference/scope_indexer"
|
|
12
13
|
require_relative "../type/nominal"
|
|
13
14
|
require_relative "../type/singleton"
|
|
@@ -99,7 +100,7 @@ module Rigor
|
|
|
99
100
|
if n.is_a?(Prism::CallNode) && n.arguments && offset_in?(n.arguments.location, cursor_offset)
|
|
100
101
|
result = n # Innermost-wins because we keep walking children.
|
|
101
102
|
end
|
|
102
|
-
n.
|
|
103
|
+
n.rigor_each_child(&walk)
|
|
103
104
|
end
|
|
104
105
|
walk.call(root)
|
|
105
106
|
result
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -8,6 +8,9 @@ require "prism"
|
|
|
8
8
|
require_relative "manifest"
|
|
9
9
|
require_relative "node_context"
|
|
10
10
|
require_relative "../analysis/diagnostic"
|
|
11
|
+
# `producer generation_cap:` defaults to (and validates against) `Cache::Store::UNBOUNDED_GENERATIONS`, and a
|
|
12
|
+
# plugin class body can be evaluated before anything else pulled the cache layer in.
|
|
13
|
+
require_relative "../cache/store"
|
|
11
14
|
require_relative "../source/node_walker"
|
|
12
15
|
|
|
13
16
|
module Rigor
|
|
@@ -86,17 +89,40 @@ module Rigor
|
|
|
86
89
|
#
|
|
87
90
|
# Producer ids are auto-prefixed `plugin.<manifest.id>.` at the cache layer (slice 6-C) so plugin-side
|
|
88
91
|
# ids cannot collide with built-in producers.
|
|
89
|
-
|
|
92
|
+
#
|
|
93
|
+
# `generation_cap:` declares how many generations of this producer's entries survive
|
|
94
|
+
# `Cache::Store#evict!`'s compaction pass. The default —
|
|
95
|
+
# `Cache::Store::UNBOUNDED_GENERATIONS` — suits the usual plugin producer, which keys per file or per
|
|
96
|
+
# discovered unit and keeps many entries live at once; only the size-based LRU pass bounds it. A
|
|
97
|
+
# producer whose entries are WHOLE-PROJECT and content-keyed (each run's inputs produce a fresh key
|
|
98
|
+
# and orphan the previous one) should declare a small positive Integer instead, so the orphans are
|
|
99
|
+
# reclaimed rather than accumulating under the global byte cap.
|
|
100
|
+
def producer(id, watch: nil, serialize: nil, deserialize: nil,
|
|
101
|
+
generation_cap: Cache::Store::UNBOUNDED_GENERATIONS, &block)
|
|
90
102
|
raise ArgumentError, "Plugin::Base.producer requires a block body" if block.nil?
|
|
91
103
|
|
|
92
104
|
validate_producer_watch!(watch)
|
|
105
|
+
validate_producer_generation_cap!(id, generation_cap)
|
|
93
106
|
@producers ||= {}
|
|
94
107
|
@producers[id.to_sym] = {
|
|
95
|
-
block: block, watch: watch, serialize: serialize, deserialize: deserialize
|
|
108
|
+
block: block, watch: watch, serialize: serialize, deserialize: deserialize,
|
|
109
|
+
generation_cap: generation_cap
|
|
96
110
|
}.freeze
|
|
97
111
|
id.to_sym
|
|
98
112
|
end
|
|
99
113
|
|
|
114
|
+
# A bad `generation_cap:` is caught at class-definition time (plugin load) rather than at the first
|
|
115
|
+
# `cache_for` round-trip, so the plugin author sees it before any caching happens.
|
|
116
|
+
def validate_producer_generation_cap!(id, generation_cap)
|
|
117
|
+
return if generation_cap == Cache::Store::UNBOUNDED_GENERATIONS
|
|
118
|
+
return if generation_cap.is_a?(Integer) && generation_cap.positive?
|
|
119
|
+
|
|
120
|
+
raise ArgumentError,
|
|
121
|
+
"Plugin::Base.producer #{id.inspect} generation_cap: must be a positive Integer or " \
|
|
122
|
+
"#{Cache::Store::UNBOUNDED_GENERATIONS.inspect}, got #{generation_cap.inspect}"
|
|
123
|
+
end
|
|
124
|
+
private :validate_producer_generation_cap!
|
|
125
|
+
|
|
100
126
|
# ADR-60 WD3 — `watch:` is nil (no glob coverage), a static tuple Array, or a Proc evaluated per
|
|
101
127
|
# `cache_for` call.
|
|
102
128
|
def validate_producer_watch!(watch)
|
|
@@ -390,8 +416,8 @@ module Rigor
|
|
|
390
416
|
# the fact slot.
|
|
391
417
|
#
|
|
392
418
|
# Renamed from `type_specifier` (ADR-80): the old name read as a parallel to `dynamic_return` (a
|
|
393
|
-
# type) when it actually returns facts.
|
|
394
|
-
# 0.
|
|
419
|
+
# type) when it actually returns facts. The old verb was a deprecating alias through 0.2.x and is
|
|
420
|
+
# gone in 0.3.0 — `narrowing_facts` is the only spelling.
|
|
395
421
|
def narrowing_facts(methods:, &block)
|
|
396
422
|
raise ArgumentError, "Plugin::Base.narrowing_facts requires a block body" if block.nil?
|
|
397
423
|
unless methods.is_a?(Array) && !methods.empty? &&
|
|
@@ -401,30 +427,17 @@ module Rigor
|
|
|
401
427
|
"got #{methods.inspect}"
|
|
402
428
|
end
|
|
403
429
|
|
|
404
|
-
@
|
|
405
|
-
@
|
|
430
|
+
@narrowing_facts_rules ||= []
|
|
431
|
+
@narrowing_facts_rules << { methods: methods.map(&:to_sym).freeze, block: block }.freeze
|
|
406
432
|
nil
|
|
407
433
|
end
|
|
408
434
|
|
|
409
|
-
#
|
|
410
|
-
# *facts*, not a type; the old name misleads by parallel with {.dynamic_return}. Retained as a
|
|
411
|
-
# warning-emitting alias through 0.2.x; REMOVED in 0.3.0. Migrate `type_specifier methods: …` →
|
|
412
|
-
# `narrowing_facts methods: …`.
|
|
413
|
-
def type_specifier(methods:, &)
|
|
414
|
-
unless @type_specifier_deprecation_warned
|
|
415
|
-
@type_specifier_deprecation_warned = true
|
|
416
|
-
warn("[rigor] Plugin::Base.type_specifier is deprecated (ADR-80) and will be " \
|
|
417
|
-
"removed in Rigor 0.3.0; rename it to `narrowing_facts`. (#{name})")
|
|
418
|
-
end
|
|
419
|
-
narrowing_facts(methods:, &)
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
# Frozen snapshot of the declared type-specifier rules. Memoised for the same reason as
|
|
435
|
+
# Frozen snapshot of the declared narrowing-facts rules. Memoised for the same reason as
|
|
423
436
|
# {dynamic_returns} — consulted per plugin per dispatch, over an array fixed at class-definition
|
|
424
437
|
# time.
|
|
425
438
|
# rubocop:disable Naming/MemoizedInstanceVariableName -- see dynamic_returns
|
|
426
|
-
def
|
|
427
|
-
@
|
|
439
|
+
def narrowing_facts_rules
|
|
440
|
+
@narrowing_facts_rules_snapshot ||= (@narrowing_facts_rules || []).dup.freeze
|
|
428
441
|
end
|
|
429
442
|
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
430
443
|
end
|
|
@@ -558,8 +571,8 @@ module Rigor
|
|
|
558
571
|
# ADR-37 slice 2 — the post-return narrowing facts contributed by this plugin's {.narrowing_facts}
|
|
559
572
|
# rules for a call. The engine calls this from `StatementEvaluator`; a rule fires only when
|
|
560
573
|
# `call_node.name` is one of its declared `methods:`. Failures isolate to [].
|
|
561
|
-
def
|
|
562
|
-
rules = self.class.
|
|
574
|
+
def narrowing_facts_for(call_node:, scope:)
|
|
575
|
+
rules = self.class.narrowing_facts_rules
|
|
563
576
|
return [] if rules.empty? || !call_node.respond_to?(:name)
|
|
564
577
|
|
|
565
578
|
name = call_node.name
|
|
@@ -741,6 +754,7 @@ module Rigor
|
|
|
741
754
|
store.fetch_or_validate(
|
|
742
755
|
producer_id: prefixed_id,
|
|
743
756
|
key_descriptor: key_descriptor,
|
|
757
|
+
generation_cap: producer[:generation_cap],
|
|
744
758
|
params: params,
|
|
745
759
|
serialize: pair_serializer(producer[:serialize]),
|
|
746
760
|
deserialize: pair_deserializer(producer[:deserialize])
|
|
@@ -18,9 +18,12 @@ module Rigor
|
|
|
18
18
|
# rules are ingested by *statically parsing* `config/initializers/inflections.rb` in a later slice, never
|
|
19
19
|
# by executing it.)
|
|
20
20
|
#
|
|
21
|
-
# **Absence is silence, never a guess.**
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# **Absence is silence, never a guess.** ActiveSupport is resolved from Rigor's host gem environment
|
|
22
|
+
# first, then from the analyzed project's own bundler install tree ({Isolation.target_bundle_root},
|
|
23
|
+
# ADR-90) — a Rails project always carries its locked activesupport on disk, and that copy is the
|
|
24
|
+
# higher-fidelity source of inflection rules anyway. When neither resolves (a standalone install
|
|
25
|
+
# analyzing a project whose bundle is not installed), the inflection methods raise {Unavailable}
|
|
26
|
+
# rather than approximate. That raise propagates to the caller's
|
|
24
27
|
# per-plugin rescue boundary, so the inflection-dependent check degrades to **no diagnostics** — reduced
|
|
25
28
|
# coverage, never a wrong fact. A consumer that wants to fail cleanly up front can gate on {available?}
|
|
26
29
|
# and emit a single load-error.
|
|
@@ -39,6 +42,12 @@ module Rigor
|
|
|
39
42
|
FEATURE = "active_support/inflector"
|
|
40
43
|
RECEIVER = "ActiveSupport::Inflector"
|
|
41
44
|
|
|
45
|
+
# The bundled plugins whose checks consume this helper (each README documents the dependency). Used
|
|
46
|
+
# by `rigor plugins` (ADR-90) to probe {available?} at activation time — so a standalone install
|
|
47
|
+
# where inflection silently degrades reports the degradation instead of an unqualified `[OK]`. When
|
|
48
|
+
# a new plugin adopts the helper, add its manifest id here.
|
|
49
|
+
CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord factorybot rails-routes].freeze
|
|
50
|
+
|
|
42
51
|
# Raised when `ActiveSupport::Inflector` is required for an inflection but cannot be loaded. Caught by
|
|
43
52
|
# the per-plugin isolation boundary, so it surfaces as "this plugin produced no diagnostics" rather than
|
|
44
53
|
# a wrong inflection.
|
|
@@ -97,8 +97,12 @@ module Rigor
|
|
|
97
97
|
private
|
|
98
98
|
|
|
99
99
|
def record_file_entry(path, contents)
|
|
100
|
+
# ADR-87 WD1 — the boundary descriptor is validation-only (it never keys a cache), so a plugin-read
|
|
101
|
+
# file rides the stat-then-digest `:stat` tier: a warm run stat-checks the (on a Rails monorepo,
|
|
102
|
+
# 30k+ file) plugin-read set instead of re-hashing it. The digest is taken from the in-memory content
|
|
103
|
+
# we just read; `FileEntry.stat` stats the same path to pack the tuple.
|
|
100
104
|
digest = Digest::SHA256.hexdigest(contents)
|
|
101
|
-
entry = Cache::Descriptor::FileEntry.
|
|
105
|
+
entry = Cache::Descriptor::FileEntry.stat(path: path, digest: digest)
|
|
102
106
|
@mutex.synchronize { @file_entries[path] = entry }
|
|
103
107
|
end
|
|
104
108
|
|