rubydex 0.4.0 → 0.4.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 +4 -20
- data/ext/rubydex/definition.c +34 -0
- data/ext/rubydex/definition.h +3 -0
- data/ext/rubydex/diagnostic.c +2 -49
- data/ext/rubydex/extconf.rb +4 -2
- data/ext/rubydex/graph.c +59 -11
- data/lib/rubydex/cli/command/lint/explain.rb +8 -6
- data/lib/rubydex/cli/command/lint.rb +10 -4
- data/lib/rubydex/linter/rule_loader.rb +1 -1
- data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
- data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
- data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
- data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
- data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
- data/lib/rubydex/rules/parse_error.rb +25 -0
- data/lib/rubydex/rules/parse_warning.rb +28 -0
- data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
- data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
- data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
- data/lib/rubydex/skill.rb +1 -0
- data/lib/rubydex/version.rb +1 -1
- data/lib/rubydex.rb +2 -0
- data/lib/rubydex_linter/rules/rule_structure.rb +19 -4
- data/rbi/rubydex.rbi +32 -130
- data/rust/rubydex/Cargo.toml +4 -0
- data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
- data/rust/rubydex/src/diagnostic.rs +142 -51
- data/rust/rubydex/src/query.rs +191 -1
- data/rust/rubydex-sys/src/definition_api.rs +68 -1
- data/rust/rubydex-sys/src/diagnostic_api.rs +0 -35
- data/rust/rubydex-sys/src/graph_api.rs +112 -11
- data/rust/rubydex-sys/src/name_api.rs +47 -11
- metadata +13 -2
data/rust/rubydex/src/query.rs
CHANGED
|
@@ -5,7 +5,7 @@ use std::thread;
|
|
|
5
5
|
|
|
6
6
|
use url::Url;
|
|
7
7
|
|
|
8
|
-
use crate::model::built_in::OBJECT_ID;
|
|
8
|
+
use crate::model::built_in::{BUILT_IN_URI_ID, OBJECT_ID};
|
|
9
9
|
use crate::model::declaration::{Ancestor, Declaration, Namespace};
|
|
10
10
|
use crate::model::definitions::{Definition, Parameter};
|
|
11
11
|
use crate::model::graph::Graph;
|
|
@@ -833,6 +833,63 @@ pub fn follow_method_alias(graph: &Graph, alias_id: DefinitionId) -> Result<Decl
|
|
|
833
833
|
}
|
|
834
834
|
}
|
|
835
835
|
|
|
836
|
+
/// Returns `true` when any definition of `declaration` was seeded by rubydex rather than read from indexed source.
|
|
837
|
+
///
|
|
838
|
+
/// Dead code candidates are reported at declaration granularity, so a seeded definition disqualifies the declaration
|
|
839
|
+
/// even when user code also reopens it.
|
|
840
|
+
fn is_built_in(graph: &Graph, declaration: &Declaration) -> bool {
|
|
841
|
+
declaration.definitions().iter().any(|definition_id| {
|
|
842
|
+
graph
|
|
843
|
+
.definitions()
|
|
844
|
+
.get(definition_id)
|
|
845
|
+
.is_some_and(|definition| definition.uri_id() == &*BUILT_IN_URI_ID)
|
|
846
|
+
})
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/// Returns a list of declarations that may be unused in the codebase, which means
|
|
850
|
+
/// that the analysis could not find any references to them. This misses
|
|
851
|
+
/// meta-programming or untyped code, which is why the term candidates is used.
|
|
852
|
+
///
|
|
853
|
+
/// Currently, only supports constants.
|
|
854
|
+
///
|
|
855
|
+
/// # Panics
|
|
856
|
+
///
|
|
857
|
+
/// Will panic if any of the threads panic
|
|
858
|
+
pub fn dead_code_candidates(graph: &Graph) -> Vec<DeclarationId> {
|
|
859
|
+
let num_threads = thread::available_parallelism().map_or(4, std::num::NonZero::get);
|
|
860
|
+
let declarations = graph.declarations();
|
|
861
|
+
|
|
862
|
+
let ids: Vec<DeclarationId> = declarations.keys().copied().collect();
|
|
863
|
+
let chunk_size = ids.len().div_ceil(num_threads);
|
|
864
|
+
|
|
865
|
+
if chunk_size == 0 {
|
|
866
|
+
return Vec::new();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
thread::scope(|s| {
|
|
870
|
+
let handles: Vec<_> = ids
|
|
871
|
+
.chunks(chunk_size)
|
|
872
|
+
.map(|chunk| {
|
|
873
|
+
s.spawn(|| {
|
|
874
|
+
chunk
|
|
875
|
+
.iter()
|
|
876
|
+
.filter(|id| {
|
|
877
|
+
let decl = declarations.get(id).unwrap();
|
|
878
|
+
decl.constant_references().is_some_and(HashSet::is_empty)
|
|
879
|
+
&& !matches!(decl, Declaration::Namespace(Namespace::SingletonClass(_)))
|
|
880
|
+
&& !decl.has_no_definitions()
|
|
881
|
+
&& !is_built_in(graph, decl)
|
|
882
|
+
})
|
|
883
|
+
.copied()
|
|
884
|
+
.collect::<Vec<_>>()
|
|
885
|
+
})
|
|
886
|
+
})
|
|
887
|
+
.collect();
|
|
888
|
+
|
|
889
|
+
handles.into_iter().flat_map(|h| h.join().unwrap()).collect()
|
|
890
|
+
})
|
|
891
|
+
}
|
|
892
|
+
|
|
836
893
|
#[cfg(test)]
|
|
837
894
|
mod tests {
|
|
838
895
|
use std::str::FromStr;
|
|
@@ -920,6 +977,21 @@ mod tests {
|
|
|
920
977
|
};
|
|
921
978
|
}
|
|
922
979
|
|
|
980
|
+
macro_rules! assert_dead_code_candidates {
|
|
981
|
+
($context:expr, [$($expected:expr),* $(,)?]) => {
|
|
982
|
+
let mut actual: Vec<String> = dead_code_candidates($context.graph())
|
|
983
|
+
.iter()
|
|
984
|
+
.map(|id| $context.graph().declarations().get(id).unwrap().name().to_string())
|
|
985
|
+
.collect();
|
|
986
|
+
actual.sort();
|
|
987
|
+
|
|
988
|
+
let mut expected: Vec<String> = vec![$(String::from($expected)),*];
|
|
989
|
+
expected.sort();
|
|
990
|
+
|
|
991
|
+
assert_eq!(expected, actual);
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
923
995
|
#[test]
|
|
924
996
|
fn fuzzy_search_returns_partial_matches() {
|
|
925
997
|
let mut context = GraphTest::new();
|
|
@@ -3982,4 +4054,122 @@ mod tests {
|
|
|
3982
4054
|
Err(FindMemberError::DeclarationNotFound),
|
|
3983
4055
|
);
|
|
3984
4056
|
}
|
|
4057
|
+
|
|
4058
|
+
#[test]
|
|
4059
|
+
fn dead_code_candidates_returns_unreferenced_constants() {
|
|
4060
|
+
let mut context = GraphTest::new();
|
|
4061
|
+
context.index_uri(
|
|
4062
|
+
"file:///foo.rb",
|
|
4063
|
+
r"
|
|
4064
|
+
class UnusedClass; end
|
|
4065
|
+
module UnusedModule; end
|
|
4066
|
+
UNUSED_CONSTANT = 1
|
|
4067
|
+
",
|
|
4068
|
+
);
|
|
4069
|
+
context.resolve();
|
|
4070
|
+
|
|
4071
|
+
assert_dead_code_candidates!(context, ["UnusedClass", "UnusedModule", "UNUSED_CONSTANT"]);
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
#[test]
|
|
4075
|
+
fn dead_code_candidates_excludes_referenced_constants() {
|
|
4076
|
+
let mut context = GraphTest::new();
|
|
4077
|
+
context.index_uri(
|
|
4078
|
+
"file:///foo.rb",
|
|
4079
|
+
r"
|
|
4080
|
+
class UsedClass; end
|
|
4081
|
+
USED_CONSTANT = 1
|
|
4082
|
+
",
|
|
4083
|
+
);
|
|
4084
|
+
context.index_uri(
|
|
4085
|
+
"file:///bar.rb",
|
|
4086
|
+
r"
|
|
4087
|
+
UsedClass
|
|
4088
|
+
USED_CONSTANT
|
|
4089
|
+
",
|
|
4090
|
+
);
|
|
4091
|
+
context.resolve();
|
|
4092
|
+
|
|
4093
|
+
assert_dead_code_candidates!(context, []);
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
#[test]
|
|
4097
|
+
fn dead_code_candidates_excludes_methods_and_variables() {
|
|
4098
|
+
let mut context = GraphTest::new();
|
|
4099
|
+
context.index_uri(
|
|
4100
|
+
"file:///foo.rb",
|
|
4101
|
+
r"
|
|
4102
|
+
$global_var = 1
|
|
4103
|
+
|
|
4104
|
+
class Holder
|
|
4105
|
+
@@class_var = 1
|
|
4106
|
+
|
|
4107
|
+
def initialize
|
|
4108
|
+
@instance_var = 1
|
|
4109
|
+
end
|
|
4110
|
+
|
|
4111
|
+
def never_called; end
|
|
4112
|
+
end
|
|
4113
|
+
",
|
|
4114
|
+
);
|
|
4115
|
+
context.resolve();
|
|
4116
|
+
|
|
4117
|
+
assert_dead_code_candidates!(context, ["Holder"]);
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
#[test]
|
|
4121
|
+
fn dead_code_candidates_counts_references_rather_than_reachability() {
|
|
4122
|
+
let mut context = GraphTest::new();
|
|
4123
|
+
context.index_uri(
|
|
4124
|
+
"file:///foo.rb",
|
|
4125
|
+
r"
|
|
4126
|
+
class Target; end
|
|
4127
|
+
AliasName = Target
|
|
4128
|
+
",
|
|
4129
|
+
);
|
|
4130
|
+
context.resolve();
|
|
4131
|
+
|
|
4132
|
+
// `AliasName` is itself dead, but it still references `Target`, giving `Target` a non-zero count.
|
|
4133
|
+
// A single pass only peels the outermost layer of a dead subgraph.
|
|
4134
|
+
assert_dead_code_candidates!(context, ["AliasName"]);
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
#[test]
|
|
4138
|
+
fn dead_code_candidates_excludes_built_ins_and_definitionless_declarations() {
|
|
4139
|
+
let mut context = GraphTest::new();
|
|
4140
|
+
context.index_uri(
|
|
4141
|
+
"file:///foo.rb",
|
|
4142
|
+
r"
|
|
4143
|
+
class Unused
|
|
4144
|
+
def self.class_method; end
|
|
4145
|
+
end
|
|
4146
|
+
|
|
4147
|
+
class Class
|
|
4148
|
+
end
|
|
4149
|
+
",
|
|
4150
|
+
);
|
|
4151
|
+
context.resolve();
|
|
4152
|
+
|
|
4153
|
+
// A reopened built-in remains excluded regardless of definition order.
|
|
4154
|
+
assert_dead_code_candidates!(context, ["Unused"]);
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
#[test]
|
|
4158
|
+
fn dead_code_candidates_excludes_singleton_classes() {
|
|
4159
|
+
let mut context = GraphTest::new();
|
|
4160
|
+
context.index_uri(
|
|
4161
|
+
"file:///foo.rb",
|
|
4162
|
+
r"
|
|
4163
|
+
class Unused; end
|
|
4164
|
+
class Attached; end
|
|
4165
|
+
|
|
4166
|
+
class << Attached
|
|
4167
|
+
def never_called; end
|
|
4168
|
+
end
|
|
4169
|
+
",
|
|
4170
|
+
);
|
|
4171
|
+
context.resolve();
|
|
4172
|
+
|
|
4173
|
+
assert_dead_code_candidates!(context, ["Unused"]);
|
|
4174
|
+
}
|
|
3985
4175
|
}
|
|
@@ -6,7 +6,9 @@ use crate::location_api::{Location, create_location_for_uri_and_offset};
|
|
|
6
6
|
use crate::reference_api::CConstantReference;
|
|
7
7
|
use libc::c_char;
|
|
8
8
|
use rubydex::model::definitions::{Definition, Mixin};
|
|
9
|
-
use rubydex::model::
|
|
9
|
+
use rubydex::model::graph::Graph;
|
|
10
|
+
use rubydex::model::ids::{DefinitionId, NameId};
|
|
11
|
+
use rubydex::model::name::ParentScope;
|
|
10
12
|
use rubydex::query::AliasResolutionError;
|
|
11
13
|
use std::ffi::CString;
|
|
12
14
|
use std::ptr;
|
|
@@ -110,6 +112,71 @@ pub unsafe extern "C" fn rdx_definition_name(pointer: GraphPointer, definition_i
|
|
|
110
112
|
})
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
fn raw_name_for_name(graph: &Graph, name_id: NameId) -> String {
|
|
116
|
+
let mut raw_name = String::new();
|
|
117
|
+
append_raw_name(graph, name_id, &mut raw_name);
|
|
118
|
+
raw_name
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn append_raw_name(graph: &Graph, name_id: NameId, raw_name: &mut String) {
|
|
122
|
+
let name = graph
|
|
123
|
+
.names()
|
|
124
|
+
.get(&name_id)
|
|
125
|
+
.expect("name should exist while building raw_name");
|
|
126
|
+
let simple_name = graph
|
|
127
|
+
.strings()
|
|
128
|
+
.get(name.str())
|
|
129
|
+
.expect("string should exist while building raw_name")
|
|
130
|
+
.as_str();
|
|
131
|
+
|
|
132
|
+
match name.parent_scope() {
|
|
133
|
+
ParentScope::None => {}
|
|
134
|
+
ParentScope::TopLevel => raw_name.push_str("::"),
|
|
135
|
+
ParentScope::Some(parent_id) | ParentScope::Attached(parent_id) => {
|
|
136
|
+
append_raw_name(graph, *parent_id, raw_name);
|
|
137
|
+
raw_name.push_str("::");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
raw_name.push_str(simple_name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn raw_name_id(definition: &Definition) -> Option<&NameId> {
|
|
145
|
+
match definition {
|
|
146
|
+
Definition::Class(_) | Definition::Module(_) | Definition::Constant(_) | Definition::ConstantAlias(_) => {
|
|
147
|
+
definition.name_id()
|
|
148
|
+
}
|
|
149
|
+
_ => None,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Returns the UTF-8 raw name string for a class, module, constant, or constant-alias definition id.
|
|
154
|
+
///
|
|
155
|
+
/// The raw name is reconstructed from the definition's `Name`, preserving explicit parent scopes like `Foo::Bar` and
|
|
156
|
+
/// rooted paths like `::Bar`. Returns null if the definition cannot be found or does not support raw names. Caller must
|
|
157
|
+
/// free a non-null result with `free_c_string`.
|
|
158
|
+
///
|
|
159
|
+
/// # Panics
|
|
160
|
+
///
|
|
161
|
+
/// Panics if the graph contains an invalid name or string reference, or if the name contains an embedded null byte.
|
|
162
|
+
///
|
|
163
|
+
/// # Safety
|
|
164
|
+
///
|
|
165
|
+
/// Assumes pointer is valid.
|
|
166
|
+
///
|
|
167
|
+
#[unsafe(no_mangle)]
|
|
168
|
+
pub unsafe extern "C" fn rdx_definition_raw_name(pointer: GraphPointer, definition_id: u64) -> *const c_char {
|
|
169
|
+
with_graph(pointer, |graph| {
|
|
170
|
+
let def_id = DefinitionId::new(definition_id);
|
|
171
|
+
let Some(name_id) = graph.definitions().get(&def_id).and_then(raw_name_id) else {
|
|
172
|
+
return ptr::null();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let name = raw_name_for_name(graph, *name_id);
|
|
176
|
+
CString::new(name).unwrap().into_raw().cast_const()
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
113
180
|
/// Shared iterator over definition (id, kind) pairs
|
|
114
181
|
#[derive(Debug)]
|
|
115
182
|
pub struct DefinitionsIter {
|
|
@@ -32,7 +32,6 @@ impl From<Severity> for DiagnosticSeverity {
|
|
|
32
32
|
pub struct CRule {
|
|
33
33
|
pub name: *const c_char,
|
|
34
34
|
pub name_length: usize,
|
|
35
|
-
pub default_severity: DiagnosticSeverity,
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
impl From<Rule> for CRule {
|
|
@@ -42,44 +41,10 @@ impl From<Rule> for CRule {
|
|
|
42
41
|
Self {
|
|
43
42
|
name: name.as_ptr().cast::<c_char>(),
|
|
44
43
|
name_length: name.len(),
|
|
45
|
-
default_severity: DiagnosticSeverity::from(rule.default_severity()),
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
#[repr(C)]
|
|
51
|
-
pub struct CRuleArray {
|
|
52
|
-
pub items: *mut CRule,
|
|
53
|
-
pub len: usize,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/// Returns every rule the graph can report. Caller must free it with `rdx_rules_free`.
|
|
57
|
-
#[unsafe(no_mangle)]
|
|
58
|
-
pub extern "C" fn rdx_rules() -> CRuleArray {
|
|
59
|
-
let items = Rule::all().iter().copied().map(CRule::from).collect::<Box<[CRule]>>();
|
|
60
|
-
|
|
61
|
-
CRuleArray {
|
|
62
|
-
len: items.len(),
|
|
63
|
-
items: Box::into_raw(items).cast::<CRule>(),
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// Frees an array previously returned by `rdx_rules`.
|
|
68
|
-
///
|
|
69
|
-
/// # Safety
|
|
70
|
-
///
|
|
71
|
-
/// - `rules` must have been returned by `rdx_rules` and must not be used afterwards.
|
|
72
|
-
#[unsafe(no_mangle)]
|
|
73
|
-
pub unsafe extern "C" fn rdx_rules_free(rules: CRuleArray) {
|
|
74
|
-
if rules.items.is_null() {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
unsafe {
|
|
79
|
-
let _ = Box::from_raw(ptr::slice_from_raw_parts_mut(rules.items, rules.len));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
48
|
/// C-compatible struct representing a diagnostic entry.
|
|
84
49
|
#[repr(C)]
|
|
85
50
|
pub struct DiagnosticEntry {
|
|
@@ -10,9 +10,10 @@ use crate::{name_api, utils};
|
|
|
10
10
|
use libc::{c_char, c_void};
|
|
11
11
|
use rubydex::config::Config;
|
|
12
12
|
use rubydex::indexing::LanguageId;
|
|
13
|
+
use rubydex::model::definitions::Definition;
|
|
13
14
|
use rubydex::model::encoding::Encoding;
|
|
14
15
|
use rubydex::model::graph::Graph;
|
|
15
|
-
use rubydex::model::ids::{DeclarationId, NameId, UriId, declaration_id_from_lookup_name};
|
|
16
|
+
use rubydex::model::ids::{DeclarationId, DefinitionId, NameId, UriId, declaration_id_from_lookup_name};
|
|
16
17
|
use rubydex::model::keywords;
|
|
17
18
|
use rubydex::model::name::NameRef;
|
|
18
19
|
use rubydex::model::visibility::Visibility;
|
|
@@ -133,6 +134,27 @@ pub unsafe extern "C" fn rdx_graph_declarations_fuzzy_search(
|
|
|
133
134
|
DeclarationsIter::new(entries)
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
/// Returns an iterator over all dead code candidates in the graph.
|
|
138
|
+
///
|
|
139
|
+
/// # Safety
|
|
140
|
+
///
|
|
141
|
+
/// Expects `pointer` to be a valid graph. The returned iterator must be freed with `rdx_graph_declarations_iter_free`.
|
|
142
|
+
#[unsafe(no_mangle)]
|
|
143
|
+
pub unsafe extern "C" fn rdx_graph_dead_code_candidates(pointer: GraphPointer) -> *mut DeclarationsIter {
|
|
144
|
+
let entries = with_graph(pointer, |graph| {
|
|
145
|
+
query::dead_code_candidates(graph)
|
|
146
|
+
.into_iter()
|
|
147
|
+
.filter_map(|id| {
|
|
148
|
+
let decl = graph.declarations().get(&id)?;
|
|
149
|
+
Some(CDeclaration::from_declaration(id, decl))
|
|
150
|
+
})
|
|
151
|
+
.collect::<Vec<CDeclaration>>()
|
|
152
|
+
.into_boxed_slice()
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
DeclarationsIter::new(entries)
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
/// # Panics
|
|
137
159
|
///
|
|
138
160
|
/// Will panic if the nesting cannot be transformed into a vector of strings
|
|
@@ -155,24 +177,103 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant(
|
|
|
155
177
|
return ptr::null();
|
|
156
178
|
};
|
|
157
179
|
|
|
158
|
-
|
|
180
|
+
resolve_constant_name(graph, name_id, names_to_untrack)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Result of resolving a constant using a namespace definition as its lexical context.
|
|
185
|
+
#[repr(u8)]
|
|
186
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
187
|
+
pub enum CDefinitionConstantResolution {
|
|
188
|
+
Resolved = 0,
|
|
189
|
+
NotFound = 1,
|
|
190
|
+
DefinitionNotFound = 2,
|
|
191
|
+
InvalidDefinitionKind = 3,
|
|
192
|
+
}
|
|
159
193
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
194
|
+
#[repr(C)]
|
|
195
|
+
#[derive(Debug)]
|
|
196
|
+
pub struct CDefinitionConstantResolutionResult {
|
|
197
|
+
pub status: CDefinitionConstantResolution,
|
|
198
|
+
pub declaration: *const CDeclaration,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Resolves a constant using a namespace definition as its lexical context.
|
|
202
|
+
///
|
|
203
|
+
/// # Panics
|
|
204
|
+
///
|
|
205
|
+
/// Panics if `const_name` cannot be converted to a string.
|
|
206
|
+
///
|
|
207
|
+
/// # Safety
|
|
208
|
+
///
|
|
209
|
+
/// Assumes that `pointer` and `const_name` are valid.
|
|
210
|
+
#[unsafe(no_mangle)]
|
|
211
|
+
pub unsafe extern "C" fn rdx_graph_resolve_constant_from_definition(
|
|
212
|
+
pointer: GraphPointer,
|
|
213
|
+
const_name: *const c_char,
|
|
214
|
+
definition_id: u64,
|
|
215
|
+
) -> CDefinitionConstantResolutionResult {
|
|
216
|
+
with_mut_graph(pointer, |graph| {
|
|
217
|
+
let const_name: String = unsafe { utils::convert_char_ptr_to_string(const_name).unwrap() };
|
|
218
|
+
let nesting = match class_or_module_definition_name_id(graph, DefinitionId::new(definition_id)) {
|
|
219
|
+
Ok(nesting) => nesting,
|
|
220
|
+
Err(status) => {
|
|
221
|
+
return CDefinitionConstantResolutionResult {
|
|
222
|
+
status,
|
|
223
|
+
declaration: ptr::null(),
|
|
224
|
+
};
|
|
164
225
|
}
|
|
165
|
-
None => ptr::null(),
|
|
166
226
|
};
|
|
167
227
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
228
|
+
let Some((name_id, names_to_untrack)) = name_api::name_in_nesting_to_name_id(graph, &const_name, Some(nesting))
|
|
229
|
+
else {
|
|
230
|
+
return CDefinitionConstantResolutionResult {
|
|
231
|
+
status: CDefinitionConstantResolution::NotFound,
|
|
232
|
+
declaration: ptr::null(),
|
|
233
|
+
};
|
|
234
|
+
};
|
|
171
235
|
|
|
172
|
-
declaration
|
|
236
|
+
let declaration = resolve_constant_name(graph, name_id, names_to_untrack);
|
|
237
|
+
let status = if declaration.is_null() {
|
|
238
|
+
CDefinitionConstantResolution::NotFound
|
|
239
|
+
} else {
|
|
240
|
+
CDefinitionConstantResolution::Resolved
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
CDefinitionConstantResolutionResult { status, declaration }
|
|
173
244
|
})
|
|
174
245
|
}
|
|
175
246
|
|
|
247
|
+
fn class_or_module_definition_name_id(
|
|
248
|
+
graph: &Graph,
|
|
249
|
+
definition_id: DefinitionId,
|
|
250
|
+
) -> Result<NameId, CDefinitionConstantResolution> {
|
|
251
|
+
let Some(definition) = graph.definitions().get(&definition_id) else {
|
|
252
|
+
return Err(CDefinitionConstantResolution::DefinitionNotFound);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
match definition {
|
|
256
|
+
Definition::Class(definition) => Ok(*definition.name_id()),
|
|
257
|
+
Definition::SingletonClass(definition) => Ok(*definition.name_id()),
|
|
258
|
+
Definition::Module(definition) => Ok(*definition.name_id()),
|
|
259
|
+
_ => Err(CDefinitionConstantResolution::InvalidDefinitionKind),
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn resolve_constant_name(graph: &mut Graph, name_id: NameId, names_to_untrack: Vec<NameId>) -> *const CDeclaration {
|
|
264
|
+
let declaration_id = Resolver::new(graph).resolve_constant(name_id);
|
|
265
|
+
let declaration = declaration_id.map_or(ptr::null(), |id| {
|
|
266
|
+
let declaration = graph.declarations().get(&id).unwrap();
|
|
267
|
+
Box::into_raw(Box::new(CDeclaration::from_declaration(id, declaration))).cast_const()
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
for name_id in names_to_untrack {
|
|
271
|
+
graph.untrack_name(name_id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
declaration
|
|
275
|
+
}
|
|
276
|
+
|
|
176
277
|
/// Adds glob patterns to exclude from file discovery during indexing.
|
|
177
278
|
///
|
|
178
279
|
/// # Panics
|
|
@@ -13,13 +13,7 @@ pub fn nesting_stack_to_name_id(
|
|
|
13
13
|
let mut names_to_untrack = Vec::new();
|
|
14
14
|
|
|
15
15
|
for entry in nesting {
|
|
16
|
-
process_qualified_name(
|
|
17
|
-
graph,
|
|
18
|
-
&entry,
|
|
19
|
-
&mut current_name,
|
|
20
|
-
&mut current_nesting,
|
|
21
|
-
&mut names_to_untrack,
|
|
22
|
-
);
|
|
16
|
+
process_qualified_name(graph, &entry, current_nesting, &mut current_name, &mut names_to_untrack);
|
|
23
17
|
current_nesting = current_name.as_ref().copied();
|
|
24
18
|
current_name = ParentScope::None;
|
|
25
19
|
}
|
|
@@ -27,12 +21,39 @@ pub fn nesting_stack_to_name_id(
|
|
|
27
21
|
process_qualified_name(
|
|
28
22
|
graph,
|
|
29
23
|
const_name,
|
|
24
|
+
current_nesting,
|
|
30
25
|
&mut current_name,
|
|
31
|
-
&mut current_nesting,
|
|
32
26
|
&mut names_to_untrack,
|
|
33
27
|
);
|
|
34
28
|
|
|
35
29
|
let (ParentScope::Some(name_id) | ParentScope::Attached(name_id)) = current_name else {
|
|
30
|
+
// Invalid names may leave temporary parts in the graph (e.g. `Foo` from `Foo::`).
|
|
31
|
+
for name_id in names_to_untrack {
|
|
32
|
+
graph.untrack_name(name_id);
|
|
33
|
+
}
|
|
34
|
+
return None;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
Some((name_id, names_to_untrack))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Takes a constant name and an existing lexical nesting and transforms it into a `NameId`, registering each required
|
|
41
|
+
/// part in the graph. Returns the `NameId` and a list of name ids that need to be untracked afterwards.
|
|
42
|
+
pub fn name_in_nesting_to_name_id(
|
|
43
|
+
graph: &mut Graph,
|
|
44
|
+
const_name: &str,
|
|
45
|
+
nesting: Option<NameId>,
|
|
46
|
+
) -> Option<(NameId, Vec<NameId>)> {
|
|
47
|
+
let mut current_name = ParentScope::None;
|
|
48
|
+
let mut names_to_untrack = Vec::new();
|
|
49
|
+
|
|
50
|
+
process_qualified_name(graph, const_name, nesting, &mut current_name, &mut names_to_untrack);
|
|
51
|
+
|
|
52
|
+
let (ParentScope::Some(name_id) | ParentScope::Attached(name_id)) = current_name else {
|
|
53
|
+
// Invalid names may leave temporary parts in the graph (e.g. `Foo` from `Foo::`).
|
|
54
|
+
for name_id in names_to_untrack {
|
|
55
|
+
graph.untrack_name(name_id);
|
|
56
|
+
}
|
|
36
57
|
return None;
|
|
37
58
|
};
|
|
38
59
|
|
|
@@ -47,8 +68,8 @@ pub fn nesting_stack_to_name_id(
|
|
|
47
68
|
fn process_qualified_name(
|
|
48
69
|
graph: &mut Graph,
|
|
49
70
|
qualified_name: &str,
|
|
71
|
+
current_nesting: Option<NameId>,
|
|
50
72
|
current_name: &mut ParentScope,
|
|
51
|
-
current_nesting: &mut Option<NameId>,
|
|
52
73
|
names_to_untrack: &mut Vec<NameId>,
|
|
53
74
|
) {
|
|
54
75
|
for part in qualified_name.split("::") {
|
|
@@ -60,13 +81,13 @@ fn process_qualified_name(
|
|
|
60
81
|
let (parent_scope, nesting_for_part) = if part.starts_with('<') {
|
|
61
82
|
let attached_id = match *current_name {
|
|
62
83
|
ParentScope::Some(id) | ParentScope::Attached(id) => Some(id),
|
|
63
|
-
_ =>
|
|
84
|
+
_ => current_nesting,
|
|
64
85
|
};
|
|
65
86
|
|
|
66
87
|
let attached = attached_id.map_or(ParentScope::None, ParentScope::Attached);
|
|
67
88
|
(attached, attached_id)
|
|
68
89
|
} else {
|
|
69
|
-
(*current_name,
|
|
90
|
+
(*current_name, current_nesting)
|
|
70
91
|
};
|
|
71
92
|
|
|
72
93
|
let str_id = graph.intern_string(part.to_owned());
|
|
@@ -182,4 +203,19 @@ mod tests {
|
|
|
182
203
|
assert!(foo_name.parent_scope().is_none());
|
|
183
204
|
assert!(foo_name.nesting().is_none());
|
|
184
205
|
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn invalid_names_are_untracked() {
|
|
209
|
+
let mut graph = Graph::new();
|
|
210
|
+
let name_count = graph.names().len();
|
|
211
|
+
let string_count = graph.strings().len();
|
|
212
|
+
|
|
213
|
+
assert!(nesting_stack_to_name_id(&mut graph, "Foo::", vec!["Bar".into()]).is_none());
|
|
214
|
+
assert_eq!(name_count, graph.names().len());
|
|
215
|
+
assert_eq!(string_count, graph.strings().len());
|
|
216
|
+
|
|
217
|
+
assert!(name_in_nesting_to_name_id(&mut graph, "Foo::", None).is_none());
|
|
218
|
+
assert_eq!(name_count, graph.names().len());
|
|
219
|
+
assert_eq!(string_count, graph.strings().len());
|
|
220
|
+
}
|
|
185
221
|
}
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubydex
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A high-performance static analysis suite for Ruby, built in Rust with
|
|
14
14
|
Ruby APIs
|
|
@@ -89,6 +89,16 @@ files:
|
|
|
89
89
|
- lib/rubydex/reference.rb
|
|
90
90
|
- lib/rubydex/related_information.rb
|
|
91
91
|
- lib/rubydex/rule.rb
|
|
92
|
+
- lib/rubydex/rules/dynamic_ancestor.rb
|
|
93
|
+
- lib/rubydex/rules/dynamic_constant_reference.rb
|
|
94
|
+
- lib/rubydex/rules/dynamic_singleton_definition.rb
|
|
95
|
+
- lib/rubydex/rules/invalid_constant_visibility.rb
|
|
96
|
+
- lib/rubydex/rules/invalid_method_visibility.rb
|
|
97
|
+
- lib/rubydex/rules/parse_error.rb
|
|
98
|
+
- lib/rubydex/rules/parse_warning.rb
|
|
99
|
+
- lib/rubydex/rules/top_level_mixin_self.rb
|
|
100
|
+
- lib/rubydex/rules/undefined_constant_visibility_target.rb
|
|
101
|
+
- lib/rubydex/rules/undefined_method_visibility_target.rb
|
|
92
102
|
- lib/rubydex/severity.rb
|
|
93
103
|
- lib/rubydex/signature.rb
|
|
94
104
|
- lib/rubydex/skill.rb
|
|
@@ -119,6 +129,7 @@ files:
|
|
|
119
129
|
- rust/rubydex-sys/src/utils.rs
|
|
120
130
|
- rust/rubydex/Cargo.toml
|
|
121
131
|
- rust/rubydex/benches/graph_memory.rs
|
|
132
|
+
- rust/rubydex/src/bin/generate_ruby_rules.rs
|
|
122
133
|
- rust/rubydex/src/compile_assertions.rs
|
|
123
134
|
- rust/rubydex/src/config.rs
|
|
124
135
|
- rust/rubydex/src/diagnostic.rs
|