Skip to content

Commit 3dba913

Browse files
authored
Merge pull request #506 from Shopify/at-private-const-def
Index calls to `private_constant` and `public_constant`
2 parents b3d022b + f02b6ae commit 3dba913

8 files changed

Lines changed: 300 additions & 14 deletions

File tree

ext/rubydex/definition.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ VALUE cSingletonClassDefinition;
1313
VALUE cModuleDefinition;
1414
VALUE cConstantDefinition;
1515
VALUE cConstantAliasDefinition;
16+
VALUE cConstantVisibilityDefinition;
1617
VALUE cMethodDefinition;
1718
VALUE cAttrAccessorDefinition;
1819
VALUE cAttrReaderDefinition;
@@ -36,6 +37,8 @@ VALUE rdxi_definition_class_for_kind(DefinitionKind kind) {
3637
return cConstantDefinition;
3738
case DefinitionKind_ConstantAlias:
3839
return cConstantAliasDefinition;
40+
case DefinitionKind_ConstantVisibility:
41+
return cConstantVisibilityDefinition;
3942
case DefinitionKind_Method:
4043
return cMethodDefinition;
4144
case DefinitionKind_AttrAccessor:
@@ -181,6 +184,7 @@ void rdxi_initialize_definition(VALUE mod) {
181184
cModuleDefinition = rb_define_class_under(mRubydex, "ModuleDefinition", cDefinition);
182185
cConstantDefinition = rb_define_class_under(mRubydex, "ConstantDefinition", cDefinition);
183186
cConstantAliasDefinition = rb_define_class_under(mRubydex, "ConstantAliasDefinition", cDefinition);
187+
cConstantVisibilityDefinition = rb_define_class_under(mRubydex, "ConstantVisibilityDefinition", cDefinition);
184188
cMethodDefinition = rb_define_class_under(mRubydex, "MethodDefinition", cDefinition);
185189
cAttrAccessorDefinition = rb_define_class_under(mRubydex, "AttrAccessorDefinition", cDefinition);
186190
cAttrReaderDefinition = rb_define_class_under(mRubydex, "AttrReaderDefinition", cDefinition);

rust/rubydex-sys/src/definition_api.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ pub enum DefinitionKind {
1717
Module = 2,
1818
Constant = 3,
1919
ConstantAlias = 4,
20-
Method = 5,
21-
AttrAccessor = 6,
22-
AttrReader = 7,
23-
AttrWriter = 8,
24-
GlobalVariable = 9,
25-
InstanceVariable = 10,
26-
ClassVariable = 11,
27-
MethodAlias = 12,
28-
GlobalVariableAlias = 13,
20+
ConstantVisibility = 5,
21+
Method = 6,
22+
AttrAccessor = 7,
23+
AttrReader = 8,
24+
AttrWriter = 9,
25+
GlobalVariable = 10,
26+
InstanceVariable = 11,
27+
ClassVariable = 12,
28+
MethodAlias = 13,
29+
GlobalVariableAlias = 14,
2930
}
3031

3132
pub(crate) fn map_definition_to_kind(defn: &Definition) -> DefinitionKind {
@@ -35,6 +36,7 @@ pub(crate) fn map_definition_to_kind(defn: &Definition) -> DefinitionKind {
3536
Definition::Module(_) => DefinitionKind::Module,
3637
Definition::Constant(_) => DefinitionKind::Constant,
3738
Definition::ConstantAlias(_) => DefinitionKind::ConstantAlias,
39+
Definition::ConstantVisibility(_) => DefinitionKind::ConstantVisibility,
3840
Definition::Method(_) => DefinitionKind::Method,
3941
Definition::AttrAccessor(_) => DefinitionKind::AttrAccessor,
4042
Definition::AttrReader(_) => DefinitionKind::AttrReader,

rust/rubydex/src/diagnostic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ rules! {
103103
DynamicSingletonDefinition;
104104
DynamicAncestor;
105105
TopLevelMixinSelf;
106+
InvalidPrivateConstant;
106107

107108
// Resolution
108109
}

rust/rubydex/src/indexing/ruby_indexer.rs

Lines changed: 193 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use crate::indexing::local_graph::LocalGraph;
55
use crate::model::comment::Comment;
66
use crate::model::definitions::{
77
AttrAccessorDefinition, AttrReaderDefinition, AttrWriterDefinition, ClassDefinition, ClassVariableDefinition,
8-
ConstantAliasDefinition, ConstantDefinition, Definition, DefinitionFlags, ExtendDefinition,
9-
GlobalVariableAliasDefinition, GlobalVariableDefinition, IncludeDefinition, InstanceVariableDefinition,
10-
MethodAliasDefinition, MethodDefinition, Mixin, ModuleDefinition, Parameter, ParameterStruct, PrependDefinition,
11-
Receiver, Signatures, SingletonClassDefinition,
8+
ConstantAliasDefinition, ConstantDefinition, ConstantVisibilityDefinition, Definition, DefinitionFlags,
9+
ExtendDefinition, GlobalVariableAliasDefinition, GlobalVariableDefinition, IncludeDefinition,
10+
InstanceVariableDefinition, MethodAliasDefinition, MethodDefinition, Mixin, ModuleDefinition, Parameter,
11+
ParameterStruct, PrependDefinition, Receiver, Signatures, SingletonClassDefinition,
1212
};
1313
use crate::model::document::Document;
1414
use crate::model::ids::{DefinitionId, NameId, StringId, UriId};
@@ -1164,6 +1164,94 @@ impl<'a> RubyIndexer<'a> {
11641164
.add_constant_reference(ConstantReference::new(new_name_id, self.uri_id, offset));
11651165
Some(new_name_id)
11661166
}
1167+
1168+
fn handle_constant_visibility(&mut self, node: &ruby_prism::CallNode, visibility: Visibility) {
1169+
let receiver = node.receiver();
1170+
1171+
let receiver_name_id = match receiver {
1172+
Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }) => {
1173+
self.index_constant_reference(&receiver.unwrap(), true)
1174+
}
1175+
Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
1176+
Some(Nesting::Method(_)) => {
1177+
// Dynamic private constant (called from a method), we ignore it but don't report an error since it's valid Ruby
1178+
// if being called from a singleton method.
1179+
1180+
return;
1181+
}
1182+
None => {
1183+
self.local_graph.add_diagnostic(
1184+
Rule::InvalidPrivateConstant,
1185+
Offset::from_prism_location(&node.location()),
1186+
"Private constant called at top level".to_string(),
1187+
);
1188+
1189+
return;
1190+
}
1191+
_ => None,
1192+
},
1193+
_ => {
1194+
self.local_graph.add_diagnostic(
1195+
Rule::InvalidPrivateConstant,
1196+
Offset::from_prism_location(&node.location()),
1197+
"Dynamic receiver for private constant".to_string(),
1198+
);
1199+
1200+
return;
1201+
}
1202+
};
1203+
1204+
let Some(arguments) = node.arguments() else {
1205+
return;
1206+
};
1207+
1208+
for argument in &arguments.arguments() {
1209+
let (name, location) = match argument {
1210+
ruby_prism::Node::SymbolNode { .. } => {
1211+
let symbol = argument.as_symbol_node().unwrap();
1212+
if let Some(value_loc) = symbol.value_loc() {
1213+
(Self::location_to_string(&value_loc), value_loc)
1214+
} else {
1215+
continue;
1216+
}
1217+
}
1218+
ruby_prism::Node::StringNode { .. } => {
1219+
let string = argument.as_string_node().unwrap();
1220+
let name = String::from_utf8_lossy(string.unescaped()).to_string();
1221+
(name, argument.location())
1222+
}
1223+
_ => {
1224+
self.local_graph.add_diagnostic(
1225+
Rule::InvalidPrivateConstant,
1226+
Offset::from_prism_location(&argument.location()),
1227+
"Private constant called with non-symbol argument".to_string(),
1228+
);
1229+
1230+
return;
1231+
}
1232+
};
1233+
1234+
let str_id = self.local_graph.intern_string(name);
1235+
let offset = Offset::from_prism_location(&location);
1236+
let definition = Definition::ConstantVisibility(Box::new(ConstantVisibilityDefinition::new(
1237+
self.local_graph.add_name(Name::new(
1238+
str_id,
1239+
receiver_name_id.map_or(ParentScope::None, ParentScope::Some),
1240+
self.current_lexical_scope_name_id(),
1241+
)),
1242+
visibility,
1243+
self.uri_id,
1244+
offset,
1245+
Vec::new(),
1246+
DefinitionFlags::empty(),
1247+
self.current_nesting_definition_id(),
1248+
)));
1249+
1250+
let definition_id = self.local_graph.add_definition(definition);
1251+
1252+
self.add_member_to_current_owner(definition_id);
1253+
}
1254+
}
11671255
}
11681256

11691257
struct CommentGroup {
@@ -1845,6 +1933,12 @@ impl Visit<'_> for RubyIndexer<'_> {
18451933

18461934
self.index_method_reference_for_call(node);
18471935
}
1936+
"private_constant" => {
1937+
self.handle_constant_visibility(node, Visibility::Private);
1938+
}
1939+
"public_constant" => {
1940+
self.handle_constant_visibility(node, Visibility::Public);
1941+
}
18481942
_ => {
18491943
// For method calls that we don't explicitly handle each part, we continue visiting their parts as we
18501944
// may discover something inside
@@ -6469,6 +6563,101 @@ mod tests {
64696563
assert_promotable!(def);
64706564
});
64716565
}
6566+
6567+
#[test]
6568+
fn index_private_constant_calls() {
6569+
let context = index_source({
6570+
r#"
6571+
module Foo
6572+
BAR = 42
6573+
BAZ = 43
6574+
FOO = 44
6575+
6576+
private_constant :BAR, :BAZ
6577+
private_constant "FOO"
6578+
6579+
class Qux
6580+
BAR = 42
6581+
BAZ = 43
6582+
6583+
Foo.public_constant :BAR
6584+
Foo.public_constant "BAZ"
6585+
end
6586+
6587+
self.private_constant :Qux
6588+
end
6589+
6590+
Foo.public_constant :BAR
6591+
"#
6592+
});
6593+
6594+
assert_no_local_diagnostics!(&context);
6595+
6596+
assert_definition_at!(&context, "6:21-6:24", ConstantVisibility, |def| {
6597+
assert_def_name_eq!(&context, def, "BAR");
6598+
assert_eq!(def.visibility(), &Visibility::Private);
6599+
});
6600+
assert_definition_at!(&context, "6:27-6:30", ConstantVisibility, |def| {
6601+
assert_def_name_eq!(&context, def, "BAZ");
6602+
assert_eq!(def.visibility(), &Visibility::Private);
6603+
});
6604+
assert_definition_at!(&context, "7:20-7:25", ConstantVisibility, |def| {
6605+
assert_def_name_eq!(&context, def, "FOO");
6606+
assert_eq!(def.visibility(), &Visibility::Private);
6607+
});
6608+
assert_definition_at!(&context, "13:26-13:29", ConstantVisibility, |def| {
6609+
assert_def_name_eq!(&context, def, "Foo::BAR");
6610+
assert_eq!(def.visibility(), &Visibility::Public);
6611+
});
6612+
assert_definition_at!(&context, "14:25-14:30", ConstantVisibility, |def| {
6613+
assert_def_name_eq!(&context, def, "Foo::BAZ");
6614+
assert_eq!(def.visibility(), &Visibility::Public);
6615+
});
6616+
assert_definition_at!(&context, "17:26-17:29", ConstantVisibility, |def| {
6617+
assert_def_name_eq!(&context, def, "Qux");
6618+
assert_eq!(def.visibility(), &Visibility::Private);
6619+
});
6620+
assert_definition_at!(&context, "20:22-20:25", ConstantVisibility, |def| {
6621+
assert_def_name_eq!(&context, def, "Foo::BAR");
6622+
assert_eq!(def.visibility(), &Visibility::Public);
6623+
});
6624+
}
6625+
6626+
#[test]
6627+
fn index_private_constant_calls_diagnostics() {
6628+
let context = index_source({
6629+
"
6630+
private_constant :NOT_INDEXED
6631+
self.private_constant :NOT_INDEXED
6632+
foo.private_constant :NOT_INDEXED # not indexed, dynamic receiver
6633+
6634+
module Foo
6635+
private_constant NOT_INDEXED, not_indexed # not indexed, not a symbol
6636+
private_constant # not indexed, no arguments
6637+
6638+
def self.qux
6639+
private_constant :Bar # not indexed, dynamic
6640+
end
6641+
6642+
def foo
6643+
private_constant :Bar # not indexed, dynamic
6644+
end
6645+
end
6646+
"
6647+
});
6648+
6649+
assert_local_diagnostics_eq!(
6650+
&context,
6651+
vec![
6652+
"invalid-private-constant: Private constant called at top level (1:1-1:30)",
6653+
"invalid-private-constant: Private constant called at top level (2:1-2:35)",
6654+
"invalid-private-constant: Dynamic receiver for private constant (3:1-3:34)",
6655+
"invalid-private-constant: Private constant called with non-symbol argument (6:20-6:31)",
6656+
]
6657+
);
6658+
6659+
assert_eq!(context.graph().definitions().len(), 3); // Foo, Foo::Qux, Foo#foo
6660+
}
64726661
}
64736662

64746663
#[cfg(test)]

0 commit comments

Comments
 (0)