rubydex 0.2.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +110 -6
- data/THIRD_PARTY_LICENSES.html +271 -2
- data/exe/rdx +2 -73
- data/ext/rubydex/config.c +140 -0
- data/ext/rubydex/config.h +16 -0
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/diagnostic.c +75 -1
- data/ext/rubydex/diagnostic.h +2 -0
- data/ext/rubydex/graph.c +27 -48
- data/ext/rubydex/graph.h +6 -0
- data/ext/rubydex/query.c +487 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +4 -0
- data/ext/rubydex/utils.c +23 -4
- data/ext/rubydex/utils.h +5 -0
- data/lib/ruby_lsp/rubydex/addon.rb +211 -0
- data/lib/rubydex/cli/command/console.rb +55 -0
- data/lib/rubydex/cli/command/lint/explain.rb +74 -0
- data/lib/rubydex/cli/command/lint.rb +202 -0
- data/lib/rubydex/cli/command/mcp.rb +30 -0
- data/lib/rubydex/cli/command/query.rb +70 -0
- data/lib/rubydex/cli/command/skill.rb +69 -0
- data/lib/rubydex/cli/command.rb +168 -0
- data/lib/rubydex/cli.rb +93 -0
- data/lib/rubydex/config.rb +59 -0
- data/lib/rubydex/diagnostic.rb +12 -3
- data/lib/rubydex/errors.rb +42 -1
- data/lib/rubydex/graph.rb +10 -3
- data/lib/rubydex/linter/custom_rule.rb +97 -0
- data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
- data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
- data/lib/rubydex/linter/rule_loader.rb +36 -0
- data/lib/rubydex/linter/rule_test_case.rb +343 -0
- data/lib/rubydex/linter/runner.rb +56 -0
- data/lib/rubydex/linter.rb +19 -0
- data/lib/rubydex/location.rb +3 -0
- data/lib/rubydex/mcp_server.rb +1 -2
- data/lib/rubydex/related_information.rb +17 -0
- data/lib/rubydex/rule.rb +33 -0
- data/lib/rubydex/severity.rb +70 -0
- data/lib/rubydex/skill.rb +88 -0
- data/lib/rubydex/skill_registry.rb +62 -0
- data/lib/rubydex/version.rb +1 -1
- data/lib/rubydex.rb +6 -0
- data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
- data/rbi/rubydex.rbi +578 -15
- data/rust/Cargo.lock +7 -0
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +20 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/config.rs +538 -157
- data/rust/rubydex/src/diagnostic.rs +66 -40
- data/rust/rubydex/src/errors.rs +0 -1
- data/rust/rubydex/src/indexing/local_graph.rs +6 -5
- data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
- data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
- data/rust/rubydex/src/lib.rs +1 -0
- data/rust/rubydex/src/listing.rs +26 -1
- data/rust/rubydex/src/main.rs +9 -128
- data/rust/rubydex/src/model/declaration.rs +301 -229
- data/rust/rubydex/src/model/definitions.rs +27 -26
- data/rust/rubydex/src/model/document.rs +43 -7
- data/rust/rubydex/src/model/graph.rs +67 -68
- data/rust/rubydex/src/model/id.rs +55 -0
- data/rust/rubydex/src/model/ids.rs +21 -9
- data/rust/rubydex/src/model/name.rs +88 -19
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
- data/rust/rubydex/src/path_helpers.rs +77 -0
- data/rust/rubydex/src/query/cypher/schema.rs +853 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +253 -0
- data/rust/rubydex/src/query/cypher.rs +54 -0
- data/rust/rubydex/src/query.rs +125 -43
- data/rust/rubydex/src/resolution.rs +368 -395
- data/rust/rubydex/src/resolution_tests.rs +504 -78
- data/rust/rubydex/src/test_utils/context.rs +2 -1
- data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
- data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
- data/rust/rubydex/tests/cli.rs +4 -4
- data/rust/rubydex-sys/src/config_api.rs +205 -0
- data/rust/rubydex-sys/src/cypher_api.rs +791 -0
- data/rust/rubydex-sys/src/declaration_api.rs +6 -3
- data/rust/rubydex-sys/src/definition_api.rs +27 -7
- data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
- data/rust/rubydex-sys/src/graph_api.rs +31 -68
- data/rust/rubydex-sys/src/lib.rs +2 -0
- data/rust/rubydex-sys/src/name_api.rs +2 -6
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- data/rust/rubydex-sys/src/utils.rs +37 -0
- data/skills/send-private-method/SKILL.md +133 -0
- metadata +37 -2
|
@@ -29,6 +29,7 @@ use crate::{
|
|
|
29
29
|
assert_mem_size,
|
|
30
30
|
model::{
|
|
31
31
|
comment::Comment,
|
|
32
|
+
id::id_from_parts,
|
|
32
33
|
ids::{self, ConstantReferenceId, DefinitionId, NameId, StringId, UriId},
|
|
33
34
|
visibility::Visibility,
|
|
34
35
|
},
|
|
@@ -673,13 +674,13 @@ impl ConstantAliasDefinition {
|
|
|
673
674
|
|
|
674
675
|
#[must_use]
|
|
675
676
|
pub fn id(&self) -> DefinitionId {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
677
|
+
id_from_parts!(
|
|
678
|
+
DefinitionId;
|
|
679
|
+
self.alias_constant.uri_id().get(),
|
|
679
680
|
self.alias_constant.offset().start(),
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
)
|
|
681
|
+
self.alias_constant.name_id().get(),
|
|
682
|
+
self.target_name_id.get(),
|
|
683
|
+
)
|
|
683
684
|
}
|
|
684
685
|
|
|
685
686
|
#[must_use]
|
|
@@ -757,7 +758,7 @@ impl ConstantVisibilityDefinition {
|
|
|
757
758
|
|
|
758
759
|
#[must_use]
|
|
759
760
|
pub fn id(&self) -> DefinitionId {
|
|
760
|
-
|
|
761
|
+
id_from_parts!(DefinitionId; self.uri_id.get(), self.offset.start(), self.target.get())
|
|
761
762
|
}
|
|
762
763
|
|
|
763
764
|
#[must_use]
|
|
@@ -837,7 +838,7 @@ impl MethodVisibilityDefinition {
|
|
|
837
838
|
|
|
838
839
|
#[must_use]
|
|
839
840
|
pub fn id(&self) -> DefinitionId {
|
|
840
|
-
|
|
841
|
+
id_from_parts!(DefinitionId; self.uri_id.get(), self.offset.start(), self.str_id.get(), self.flags.bits())
|
|
841
842
|
}
|
|
842
843
|
|
|
843
844
|
#[must_use]
|
|
@@ -1123,7 +1124,7 @@ impl AttrAccessorDefinition {
|
|
|
1123
1124
|
|
|
1124
1125
|
#[must_use]
|
|
1125
1126
|
pub fn id(&self) -> DefinitionId {
|
|
1126
|
-
|
|
1127
|
+
id_from_parts!(DefinitionId; self.uri_id.get(), self.offset.start(), self.str_id.get())
|
|
1127
1128
|
}
|
|
1128
1129
|
|
|
1129
1130
|
#[must_use]
|
|
@@ -1204,7 +1205,7 @@ impl AttrReaderDefinition {
|
|
|
1204
1205
|
|
|
1205
1206
|
#[must_use]
|
|
1206
1207
|
pub fn id(&self) -> DefinitionId {
|
|
1207
|
-
|
|
1208
|
+
id_from_parts!(DefinitionId; self.uri_id.get(), self.offset.start(), self.str_id.get())
|
|
1208
1209
|
}
|
|
1209
1210
|
|
|
1210
1211
|
#[must_use]
|
|
@@ -1285,7 +1286,7 @@ impl AttrWriterDefinition {
|
|
|
1285
1286
|
|
|
1286
1287
|
#[must_use]
|
|
1287
1288
|
pub fn id(&self) -> DefinitionId {
|
|
1288
|
-
|
|
1289
|
+
id_from_parts!(DefinitionId; self.uri_id.get(), self.offset.start(), self.str_id.get())
|
|
1289
1290
|
}
|
|
1290
1291
|
|
|
1291
1292
|
#[must_use]
|
|
@@ -1363,7 +1364,7 @@ impl GlobalVariableDefinition {
|
|
|
1363
1364
|
|
|
1364
1365
|
#[must_use]
|
|
1365
1366
|
pub fn id(&self) -> DefinitionId {
|
|
1366
|
-
|
|
1367
|
+
id_from_parts!(DefinitionId; self.uri_id.get(), self.offset.start(), self.str_id.get())
|
|
1367
1368
|
}
|
|
1368
1369
|
|
|
1369
1370
|
#[must_use]
|
|
@@ -1436,7 +1437,7 @@ impl InstanceVariableDefinition {
|
|
|
1436
1437
|
|
|
1437
1438
|
#[must_use]
|
|
1438
1439
|
pub fn id(&self) -> DefinitionId {
|
|
1439
|
-
|
|
1440
|
+
id_from_parts!(DefinitionId; self.uri_id.get(), self.offset.start(), self.str_id.get())
|
|
1440
1441
|
}
|
|
1441
1442
|
|
|
1442
1443
|
#[must_use]
|
|
@@ -1509,7 +1510,7 @@ impl ClassVariableDefinition {
|
|
|
1509
1510
|
|
|
1510
1511
|
#[must_use]
|
|
1511
1512
|
pub fn id(&self) -> DefinitionId {
|
|
1512
|
-
|
|
1513
|
+
id_from_parts!(DefinitionId; self.uri_id.get(), self.offset.start(), self.str_id.get())
|
|
1513
1514
|
}
|
|
1514
1515
|
|
|
1515
1516
|
#[must_use]
|
|
@@ -1583,13 +1584,13 @@ impl MethodAliasDefinition {
|
|
|
1583
1584
|
|
|
1584
1585
|
#[must_use]
|
|
1585
1586
|
pub fn id(&self) -> DefinitionId {
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1587
|
+
id_from_parts!(
|
|
1588
|
+
DefinitionId;
|
|
1589
|
+
self.uri_id.get(),
|
|
1589
1590
|
self.offset.start(),
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
)
|
|
1591
|
+
self.new_name_str_id.get(),
|
|
1592
|
+
self.old_name_str_id.get(),
|
|
1593
|
+
)
|
|
1593
1594
|
}
|
|
1594
1595
|
|
|
1595
1596
|
#[must_use]
|
|
@@ -1669,13 +1670,13 @@ impl GlobalVariableAliasDefinition {
|
|
|
1669
1670
|
|
|
1670
1671
|
#[must_use]
|
|
1671
1672
|
pub fn id(&self) -> DefinitionId {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1673
|
+
id_from_parts!(
|
|
1674
|
+
DefinitionId;
|
|
1675
|
+
self.uri_id.get(),
|
|
1675
1676
|
self.offset.start(),
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
)
|
|
1677
|
+
self.new_name_str_id.get(),
|
|
1678
|
+
self.old_name_str_id.get(),
|
|
1679
|
+
)
|
|
1679
1680
|
}
|
|
1680
1681
|
|
|
1681
1682
|
#[must_use]
|
|
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
|
|
2
2
|
|
|
3
3
|
use line_index::LineIndex;
|
|
4
4
|
use url::Url;
|
|
5
|
+
use xxhash_rust::xxh3::xxh3_64;
|
|
5
6
|
|
|
6
7
|
use crate::assert_mem_size;
|
|
7
8
|
use crate::diagnostic::Diagnostic;
|
|
@@ -17,8 +18,9 @@ pub struct Document {
|
|
|
17
18
|
method_reference_ids: Vec<MethodReferenceId>,
|
|
18
19
|
constant_reference_ids: Vec<ConstantReferenceId>,
|
|
19
20
|
diagnostics: Vec<Diagnostic>,
|
|
21
|
+
content_hash: u64,
|
|
20
22
|
}
|
|
21
|
-
assert_mem_size!(Document,
|
|
23
|
+
assert_mem_size!(Document, 184);
|
|
22
24
|
|
|
23
25
|
impl Document {
|
|
24
26
|
#[must_use]
|
|
@@ -30,6 +32,7 @@ impl Document {
|
|
|
30
32
|
method_reference_ids: Vec::new(),
|
|
31
33
|
constant_reference_ids: Vec::new(),
|
|
32
34
|
diagnostics: Vec::new(),
|
|
35
|
+
content_hash: xxh3_64(source.as_bytes()),
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -38,6 +41,11 @@ impl Document {
|
|
|
38
41
|
&self.uri
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
#[must_use]
|
|
45
|
+
pub fn content_hash(&self) -> u64 {
|
|
46
|
+
self.content_hash
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
#[must_use]
|
|
42
50
|
pub fn line_index(&self) -> &LineIndex {
|
|
43
51
|
&self.line_index
|
|
@@ -84,6 +92,39 @@ impl Document {
|
|
|
84
92
|
self.diagnostics.push(diagnostic);
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
/// The file-system path of this document, decoded from its URI.
|
|
96
|
+
///
|
|
97
|
+
/// Returns `None` when the URI is not a `file://` URL (e.g. the synthetic built-in document) or
|
|
98
|
+
/// cannot be converted to a path. Uses `Url` so percent-encoding and platform-specific paths
|
|
99
|
+
/// (including Windows drive paths) are handled correctly.
|
|
100
|
+
#[must_use]
|
|
101
|
+
pub fn file_path(&self) -> Option<PathBuf> {
|
|
102
|
+
let url = Url::parse(&self.uri).ok()?;
|
|
103
|
+
if url.scheme() != "file" {
|
|
104
|
+
return None;
|
|
105
|
+
}
|
|
106
|
+
url.to_file_path().ok()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The base file name of this document (the last path segment), decoded from its URI.
|
|
110
|
+
///
|
|
111
|
+
/// Prefers the platform file path, but falls back to the last URL path segment so it still works
|
|
112
|
+
/// for `file://` URIs that don't convert to a local path on the current platform (e.g. a
|
|
113
|
+
/// drive-less path like `file:///foo.rb` on Windows). Returns `None` only when the URI has no
|
|
114
|
+
/// usable path segment (e.g. the synthetic built-in document).
|
|
115
|
+
#[must_use]
|
|
116
|
+
pub fn file_name(&self) -> Option<String> {
|
|
117
|
+
if let Some(path) = self.file_path()
|
|
118
|
+
&& let Some(name) = path.file_name()
|
|
119
|
+
{
|
|
120
|
+
return Some(name.to_string_lossy().into_owned());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let url = Url::parse(&self.uri).ok()?;
|
|
124
|
+
let segment = url.path_segments()?.rfind(|segment| !segment.is_empty())?;
|
|
125
|
+
Some(segment.to_string())
|
|
126
|
+
}
|
|
127
|
+
|
|
87
128
|
/// Computes the require path for this document given load paths.
|
|
88
129
|
///
|
|
89
130
|
/// Returns `None` if:
|
|
@@ -97,12 +138,7 @@ impl Document {
|
|
|
97
138
|
/// Panics if load path entries exceed u16.
|
|
98
139
|
#[must_use]
|
|
99
140
|
pub fn require_path(&self, load_paths: &[PathBuf]) -> Option<(String, u16)> {
|
|
100
|
-
let
|
|
101
|
-
if url.scheme() != "file" {
|
|
102
|
-
return None;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let file_path = url.to_file_path().ok()?;
|
|
141
|
+
let file_path = self.file_path()?;
|
|
106
142
|
if file_path.extension().is_none_or(|ext| ext != "rb") {
|
|
107
143
|
return None;
|
|
108
144
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
use std::collections::HashSet;
|
|
2
2
|
use std::collections::hash_map::Entry;
|
|
3
|
-
use std::path::
|
|
3
|
+
use std::path::Path;
|
|
4
4
|
|
|
5
|
-
use crate::assert_mem_size;
|
|
6
5
|
use crate::config::Config;
|
|
7
6
|
use crate::diagnostic::Diagnostic;
|
|
8
|
-
use crate::errors::Errors;
|
|
9
7
|
use crate::indexing::local_graph::LocalGraph;
|
|
10
8
|
use crate::model::built_in::{OBJECT_ID, add_built_in_data};
|
|
11
9
|
use crate::model::declaration::{Ancestor, Declaration, Namespace};
|
|
@@ -21,6 +19,7 @@ use crate::model::name::{Name, NameRef, ParentScope, ResolvedName};
|
|
|
21
19
|
use crate::model::references::{ConstantReference, MethodRef};
|
|
22
20
|
use crate::model::string_ref::StringRef;
|
|
23
21
|
use crate::model::visibility::Visibility;
|
|
22
|
+
use crate::{assert_mem_size, assert_send_sync};
|
|
24
23
|
use crate::{query, stats};
|
|
25
24
|
|
|
26
25
|
/// An entity whose validity depends on a particular `NameId`.
|
|
@@ -93,7 +92,8 @@ pub struct Graph {
|
|
|
93
92
|
/// Project configuration
|
|
94
93
|
config: Config,
|
|
95
94
|
}
|
|
96
|
-
assert_mem_size!(Graph,
|
|
95
|
+
assert_mem_size!(Graph, 368);
|
|
96
|
+
assert_send_sync!(Graph);
|
|
97
97
|
|
|
98
98
|
impl Graph {
|
|
99
99
|
#[must_use]
|
|
@@ -109,7 +109,7 @@ impl Graph {
|
|
|
109
109
|
position_encoding: Encoding::default(),
|
|
110
110
|
name_dependents: IdentityHashMap::default(),
|
|
111
111
|
pending_work: Vec::default(),
|
|
112
|
-
config: Config::
|
|
112
|
+
config: Config::default(),
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
add_built_in_data(&mut graph);
|
|
@@ -146,25 +146,9 @@ impl Graph {
|
|
|
146
146
|
self.config.workspace_path()
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
///
|
|
150
|
-
pub fn
|
|
151
|
-
self.config.
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/// Loads a configuration file. Pass `None` to load the default `rubydex.toml` configuration file if it exists
|
|
155
|
-
///
|
|
156
|
-
/// # Errors
|
|
157
|
-
///
|
|
158
|
-
/// Returns an [`Errors::ConfigNotFound`] if an explicitly requested file does not exist or an
|
|
159
|
-
/// [`Errors::ConfigError`] if a file cannot otherwise be read or its contents are malformed.
|
|
160
|
-
pub fn load_config(&mut self, config_path: Option<&Path>) -> Result<(), Errors> {
|
|
161
|
-
match config_path {
|
|
162
|
-
Some(path) => {
|
|
163
|
-
let path = self.config.workspace_path().join(path);
|
|
164
|
-
self.config.load_file(&path)
|
|
165
|
-
}
|
|
166
|
-
None => self.config.load_default(),
|
|
167
|
-
}
|
|
149
|
+
/// Loads a config for the graph
|
|
150
|
+
pub fn load_config(&mut self, config: &Config) {
|
|
151
|
+
self.config = config.clone();
|
|
168
152
|
}
|
|
169
153
|
|
|
170
154
|
/// # Panics
|
|
@@ -206,7 +190,15 @@ impl Graph {
|
|
|
206
190
|
if should_promote {
|
|
207
191
|
let mut new_declaration = constructor(fully_qualified_name);
|
|
208
192
|
let removed_declaration = occupied_entry.remove();
|
|
209
|
-
|
|
193
|
+
let singleton_class_id = removed_declaration
|
|
194
|
+
.as_namespace()
|
|
195
|
+
.and_then(Namespace::singleton_class)
|
|
196
|
+
.copied();
|
|
197
|
+
let new_namespace = new_declaration.as_namespace_mut().unwrap();
|
|
198
|
+
new_namespace.extend(removed_declaration);
|
|
199
|
+
if let Some(singleton_class_id) = singleton_class_id {
|
|
200
|
+
new_namespace.set_singleton_class_id(singleton_class_id);
|
|
201
|
+
}
|
|
210
202
|
new_declaration.add_definition(definition_id);
|
|
211
203
|
self.declarations.insert(declaration_id, new_declaration);
|
|
212
204
|
} else {
|
|
@@ -539,7 +531,8 @@ impl Graph {
|
|
|
539
531
|
/// Registers a name in the graph unless already registered. In regular indexing, this only happens in the local
|
|
540
532
|
/// graph. This method is only used to back the `Graph#resolve_constant` Ruby API because every name must be
|
|
541
533
|
/// registered in the graph to properly resolve
|
|
542
|
-
pub fn add_name(&mut self,
|
|
534
|
+
pub fn add_name(&mut self, str: StringId, parent_scope: ParentScope, nesting: Option<NameId>) -> NameId {
|
|
535
|
+
let name = Name::new(&self.names, str, parent_scope, nesting);
|
|
543
536
|
let name_id = name.id();
|
|
544
537
|
|
|
545
538
|
match self.names.entry(name_id) {
|
|
@@ -554,31 +547,6 @@ impl Graph {
|
|
|
554
547
|
name_id
|
|
555
548
|
}
|
|
556
549
|
|
|
557
|
-
/// Searches for the initial attached object for an arbitrarily nested singleton class.
|
|
558
|
-
/// Walks up the owner chain until finding a non-singleton namespace.
|
|
559
|
-
///
|
|
560
|
-
/// # Example
|
|
561
|
-
/// For `Foo::<Foo>::<<Foo>>`, returns `Foo`
|
|
562
|
-
///
|
|
563
|
-
/// # Panics
|
|
564
|
-
///
|
|
565
|
-
/// Panics if we attached a singleton class to something that isn't a namespace
|
|
566
|
-
#[must_use]
|
|
567
|
-
pub fn attached_object<'a>(&'a self, maybe_singleton: &'a Namespace) -> &'a Namespace {
|
|
568
|
-
let mut attached_object = maybe_singleton;
|
|
569
|
-
|
|
570
|
-
while matches!(attached_object, Namespace::SingletonClass(_)) {
|
|
571
|
-
attached_object = self
|
|
572
|
-
.declarations
|
|
573
|
-
.get(attached_object.owner_id())
|
|
574
|
-
.unwrap()
|
|
575
|
-
.as_namespace()
|
|
576
|
-
.unwrap();
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
attached_object
|
|
580
|
-
}
|
|
581
|
-
|
|
582
550
|
#[must_use]
|
|
583
551
|
pub fn get(&self, name: &str) -> Option<Vec<&Definition>> {
|
|
584
552
|
let declaration_id = declaration_id_from_lookup_name(name);
|
|
@@ -676,6 +644,9 @@ impl Graph {
|
|
|
676
644
|
///
|
|
677
645
|
/// For methods, the latest definition wins. For constants, the latest
|
|
678
646
|
/// `private_constant`/`public_constant` wins, otherwise `Public`.
|
|
647
|
+
///
|
|
648
|
+
/// Methods declared via `module_function :bar` return `Private` on the instance
|
|
649
|
+
/// side (`Foo#bar`) and `Public` on the singleton side (`Foo::<Foo>#bar`).
|
|
679
650
|
#[must_use]
|
|
680
651
|
pub fn visibility(&self, declaration_id: &DeclarationId) -> Option<Visibility> {
|
|
681
652
|
let declaration = self.declarations.get(declaration_id)?;
|
|
@@ -701,7 +672,13 @@ impl Graph {
|
|
|
701
672
|
};
|
|
702
673
|
|
|
703
674
|
let visibility = match definition {
|
|
704
|
-
Definition::MethodVisibility(vis) =>
|
|
675
|
+
Definition::MethodVisibility(vis) => match *vis.visibility() {
|
|
676
|
+
Visibility::ModuleFunction if vis.flags().is_singleton_method_visibility() => {
|
|
677
|
+
Some(Visibility::Public)
|
|
678
|
+
}
|
|
679
|
+
Visibility::ModuleFunction => Some(Visibility::Private),
|
|
680
|
+
other => Some(other),
|
|
681
|
+
},
|
|
705
682
|
Definition::Method(method) => Some(*method.visibility()),
|
|
706
683
|
Definition::AttrAccessor(attr) => Some(*attr.visibility()),
|
|
707
684
|
Definition::AttrReader(attr) => Some(*attr.visibility()),
|
|
@@ -908,12 +885,7 @@ impl Graph {
|
|
|
908
885
|
) {
|
|
909
886
|
if let Some(declaration) = self.declarations.get_mut(owner_id) {
|
|
910
887
|
match declaration {
|
|
911
|
-
Declaration::Namespace(
|
|
912
|
-
Declaration::Namespace(Namespace::Module(it)) => it.add_member(member_str_id, member_declaration_id),
|
|
913
|
-
Declaration::Namespace(Namespace::SingletonClass(it)) => {
|
|
914
|
-
it.add_member(member_str_id, member_declaration_id);
|
|
915
|
-
}
|
|
916
|
-
Declaration::Namespace(Namespace::Todo(it)) => it.add_member(member_str_id, member_declaration_id),
|
|
888
|
+
Declaration::Namespace(namespace) => namespace.add_member(member_str_id, member_declaration_id),
|
|
917
889
|
Declaration::Constant(_) => {
|
|
918
890
|
// TODO: temporary hack to avoid crashing on `Struct.new`, `Class.new` and `Module.new`
|
|
919
891
|
}
|
|
@@ -1044,7 +1016,17 @@ impl Graph {
|
|
|
1044
1016
|
/// 3. `extend` -- merges the new `LocalGraph` into the now-clean graph
|
|
1045
1017
|
pub fn consume_document_changes(&mut self, other: LocalGraph) {
|
|
1046
1018
|
let uri_id = other.uri_id();
|
|
1047
|
-
|
|
1019
|
+
|
|
1020
|
+
let old_document = match self.documents.entry(uri_id) {
|
|
1021
|
+
Entry::Occupied(entry) => {
|
|
1022
|
+
// No changes to the document, skip invalidation and merging
|
|
1023
|
+
if entry.get().content_hash() == other.document().content_hash() {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
Some(entry.remove())
|
|
1027
|
+
}
|
|
1028
|
+
Entry::Vacant(_) => None,
|
|
1029
|
+
};
|
|
1048
1030
|
|
|
1049
1031
|
// Skip invalidation during boot indexing (no documents have been resolved yet)
|
|
1050
1032
|
// or when the document is brand new (no old data to invalidate against).
|
|
@@ -1298,7 +1280,7 @@ impl Graph {
|
|
|
1298
1280
|
&& let Some(anc_decl) = self.declarations.get_mut(&ancestor_id)
|
|
1299
1281
|
&& let Some(ns) = anc_decl.as_namespace_mut()
|
|
1300
1282
|
{
|
|
1301
|
-
ns.remove_descendant(
|
|
1283
|
+
ns.remove_descendant(decl_id);
|
|
1302
1284
|
}
|
|
1303
1285
|
}
|
|
1304
1286
|
}
|
|
@@ -1322,7 +1304,7 @@ impl Graph {
|
|
|
1322
1304
|
&& let Some(anc_decl) = self.declarations.get_mut(ancestor_id)
|
|
1323
1305
|
&& let Some(ns) = anc_decl.as_namespace_mut()
|
|
1324
1306
|
{
|
|
1325
|
-
ns.remove_descendant(
|
|
1307
|
+
ns.remove_descendant(decl_id);
|
|
1326
1308
|
}
|
|
1327
1309
|
}
|
|
1328
1310
|
|
|
@@ -1816,7 +1798,7 @@ mod tests {
|
|
|
1816
1798
|
context.index_uri("file:///a.rb", "");
|
|
1817
1799
|
|
|
1818
1800
|
{
|
|
1819
|
-
let Declaration::Namespace(Namespace::Class(
|
|
1801
|
+
let Declaration::Namespace(foo @ Namespace::Class(_)) =
|
|
1820
1802
|
context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap()
|
|
1821
1803
|
else {
|
|
1822
1804
|
panic!("Expected Foo to be a class");
|
|
@@ -1824,7 +1806,7 @@ mod tests {
|
|
|
1824
1806
|
assert!(matches!(foo.ancestors(), Ancestors::Partial(a) if a.is_empty()));
|
|
1825
1807
|
assert!(foo.descendants().is_empty());
|
|
1826
1808
|
|
|
1827
|
-
let Declaration::Namespace(Namespace::Class(
|
|
1809
|
+
let Declaration::Namespace(baz @ Namespace::Class(_)) =
|
|
1828
1810
|
context.graph().declarations().get(&DeclarationId::from("Baz")).unwrap()
|
|
1829
1811
|
else {
|
|
1830
1812
|
panic!("Expected Baz to be a class");
|
|
@@ -1832,7 +1814,7 @@ mod tests {
|
|
|
1832
1814
|
assert!(matches!(baz.ancestors(), Ancestors::Partial(a) if a.is_empty()));
|
|
1833
1815
|
assert!(baz.descendants().is_empty());
|
|
1834
1816
|
|
|
1835
|
-
let Declaration::Namespace(Namespace::Module(
|
|
1817
|
+
let Declaration::Namespace(bar @ Namespace::Module(_)) =
|
|
1836
1818
|
context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap()
|
|
1837
1819
|
else {
|
|
1838
1820
|
panic!("Expected Bar to be a module");
|
|
@@ -2203,9 +2185,9 @@ mod tests {
|
|
|
2203
2185
|
|
|
2204
2186
|
assert_eq!(
|
|
2205
2187
|
vec![
|
|
2206
|
-
"
|
|
2207
|
-
"
|
|
2208
|
-
"
|
|
2188
|
+
"ParseError: expected an `end` to close the `class` statement (file:///foo1.rb)",
|
|
2189
|
+
"ParseError: unexpected end-of-input, assuming it is closing the parent top level context (file:///foo1.rb)",
|
|
2190
|
+
"ParseWarning: assigned but unused variable - foo (file:///foo2.rb)",
|
|
2209
2191
|
],
|
|
2210
2192
|
diagnostics,
|
|
2211
2193
|
);
|
|
@@ -2527,7 +2509,7 @@ mod tests {
|
|
|
2527
2509
|
);
|
|
2528
2510
|
|
|
2529
2511
|
// Delete bar.rb — the Bar name should be fully removed
|
|
2530
|
-
let bar_name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
|
|
2512
|
+
let bar_name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
|
|
2531
2513
|
context.index_uri("file:///bar.rb", "");
|
|
2532
2514
|
context.resolve();
|
|
2533
2515
|
|
|
@@ -4209,4 +4191,21 @@ mod incremental_resolution_tests {
|
|
|
4209
4191
|
);
|
|
4210
4192
|
}
|
|
4211
4193
|
}
|
|
4194
|
+
|
|
4195
|
+
#[test]
|
|
4196
|
+
fn unchanged_document_does_not_trigger_invalidation() {
|
|
4197
|
+
let mut context = GraphTest::new();
|
|
4198
|
+
context.index_uri("file:///a.rb", "class Foo; end");
|
|
4199
|
+
context.resolve();
|
|
4200
|
+
|
|
4201
|
+
// Re-indexing the same content should not trigger any invalidation or changes
|
|
4202
|
+
context.index_uri("file:///a.rb", "class Foo; end");
|
|
4203
|
+
|
|
4204
|
+
let mut graph = context.into_graph();
|
|
4205
|
+
|
|
4206
|
+
assert!(
|
|
4207
|
+
graph.take_pending_work().is_empty(),
|
|
4208
|
+
"Graph should have no pending work after re-indexing unchanged document"
|
|
4209
|
+
);
|
|
4210
|
+
}
|
|
4212
4211
|
} // mod incremental_resolution_tests
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
use std::{marker::PhantomData, num::NonZeroU64, ops::Deref};
|
|
2
2
|
use xxhash_rust::xxh3;
|
|
3
3
|
|
|
4
|
+
/// Creates an ID by hashing fixed-width integer components in little-endian order.
|
|
5
|
+
macro_rules! id_from_parts {
|
|
6
|
+
($id:ty; $($part:expr),+ $(,)?) => {
|
|
7
|
+
<$id>::from([$(($part).to_le_bytes().as_slice()),+])
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
pub(crate) use id_from_parts;
|
|
11
|
+
|
|
4
12
|
/// Maps a u64 hash to a `NonZeroU64` by replacing 0 with `u64::MAX`.
|
|
5
13
|
/// The probability of a 64-bit hash being exactly 0 is 2^-64 (~5.4e-20),
|
|
6
14
|
/// and remapping 0 → MAX just means those two inputs collide — the same
|
|
@@ -70,6 +78,18 @@ impl<T> From<&String> for Id<T> {
|
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
impl<T, const N: usize> From<[&[u8]; N]> for Id<T> {
|
|
82
|
+
fn from(parts: [&[u8]; N]) -> Self {
|
|
83
|
+
let mut hasher = xxh3::Xxh3Default::new();
|
|
84
|
+
|
|
85
|
+
for part in parts {
|
|
86
|
+
hasher.update(part);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Self::new(hasher.digest())
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
#[cfg(test)]
|
|
74
94
|
mod tests {
|
|
75
95
|
use super::*;
|
|
@@ -78,6 +98,41 @@ mod tests {
|
|
|
78
98
|
pub struct Marker;
|
|
79
99
|
pub type TestId = Id<Marker>;
|
|
80
100
|
|
|
101
|
+
#[test]
|
|
102
|
+
fn from_byte_slices_matches_concatenated_string() {
|
|
103
|
+
assert_eq!(
|
|
104
|
+
TestId::from("foobar"),
|
|
105
|
+
TestId::from([b"foo".as_slice(), b"bar".as_slice()]),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[test]
|
|
110
|
+
fn from_parts_matches_mixed_width_byte_slices() {
|
|
111
|
+
let id = 42_u64;
|
|
112
|
+
let offset = 7_u32;
|
|
113
|
+
let tag = 2_u8;
|
|
114
|
+
|
|
115
|
+
assert_eq!(
|
|
116
|
+
TestId::from([
|
|
117
|
+
id.to_le_bytes().as_slice(),
|
|
118
|
+
offset.to_le_bytes().as_slice(),
|
|
119
|
+
tag.to_le_bytes().as_slice(),
|
|
120
|
+
]),
|
|
121
|
+
id_from_parts!(TestId; id, offset, tag),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn from_byte_slices_is_deterministic() {
|
|
127
|
+
let parts: [&[u8]; 2] = [&[1, 2, 3], &[4, 5]];
|
|
128
|
+
assert_eq!(TestId::from(parts), TestId::from(parts));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn from_byte_slices_distinguishes_inputs() {
|
|
133
|
+
assert_ne!(TestId::from([b"foo".as_slice()]), TestId::from([b"bar".as_slice()]),);
|
|
134
|
+
}
|
|
135
|
+
|
|
81
136
|
#[test]
|
|
82
137
|
fn test_create_hash() {
|
|
83
138
|
// Same input should produce same hash (deterministic)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
use crate::{
|
|
2
2
|
assert_mem_size,
|
|
3
|
-
model::{
|
|
3
|
+
model::{
|
|
4
|
+
definitions::Receiver,
|
|
5
|
+
id::{Id, id_from_parts},
|
|
6
|
+
},
|
|
4
7
|
offset::Offset,
|
|
5
8
|
};
|
|
6
9
|
|
|
@@ -27,7 +30,7 @@ assert_mem_size!(DefinitionId, 8);
|
|
|
27
30
|
|
|
28
31
|
#[must_use]
|
|
29
32
|
pub fn namespace_definition_id(uri_id: UriId, offset: &Offset, name_id: NameId) -> DefinitionId {
|
|
30
|
-
|
|
33
|
+
id_from_parts!(DefinitionId; uri_id.get(), offset.start(), name_id.get())
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
#[must_use]
|
|
@@ -37,14 +40,23 @@ pub fn method_definition_id(
|
|
|
37
40
|
str_id: StringId,
|
|
38
41
|
receiver: Option<&Receiver>,
|
|
39
42
|
) -> DefinitionId {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
match receiver {
|
|
44
|
+
Some(Receiver::SelfReceiver(def_id)) => id_from_parts!(
|
|
45
|
+
DefinitionId;
|
|
46
|
+
uri_id.get(),
|
|
47
|
+
offset.start(),
|
|
48
|
+
str_id.get(),
|
|
49
|
+
def_id.get(),
|
|
50
|
+
),
|
|
51
|
+
Some(Receiver::ConstantReceiver(name_id)) => id_from_parts!(
|
|
52
|
+
DefinitionId;
|
|
53
|
+
uri_id.get(),
|
|
54
|
+
offset.start(),
|
|
55
|
+
str_id.get(),
|
|
56
|
+
name_id.get(),
|
|
57
|
+
),
|
|
58
|
+
None => id_from_parts!(DefinitionId; uri_id.get(), offset.start(), str_id.get()),
|
|
46
59
|
}
|
|
47
|
-
DefinitionId::from(&formatted_id)
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
|