Skip to content

Commit 6dedd5c

Browse files
authored
Reject Leo keywords as package names (#29496)
* Reject Leo keywords as package names * Share lexer keyword checks
1 parent 90ac343 commit 6dedd5c

7 files changed

Lines changed: 97 additions & 63 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/package/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ edition = "2024"
2121
# leo dependencies
2222
leo-ast = { workspace = true }
2323
leo-errors = { workspace = true }
24+
leo-parser-rowan = { workspace = true }
2425
leo-span = { workspace = true }
2526
# third party dependencies
2627
glob = { workspace = true }

crates/package/src/lib.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,15 @@ fn is_valid_package_name(name: &str) -> bool {
202202
return false;
203203
}
204204

205-
// Check reserved keywords.
206-
if reserved_keywords().any(|kw| kw == name) {
205+
if is_leo_keyword(name) {
206+
tracing::error!("Aleo names cannot be a Leo keyword.");
207+
return false;
208+
}
209+
210+
if is_aleo_keyword(name) {
207211
tracing::error!(
208212
"Aleo names cannot be a SnarkVM reserved keyword. Reserved keywords are: {}.",
209-
reserved_keywords().collect::<Vec<_>>().join(", ")
213+
aleo_reserved_keywords().collect::<Vec<_>>().join(", ")
210214
);
211215
return false;
212216
}
@@ -223,7 +227,7 @@ fn is_valid_package_name(name: &str) -> bool {
223227
/// Get the list of all reserved and restricted keywords from snarkVM.
224228
/// These keywords cannot be used as program names.
225229
/// See: https://github.com/ProvableHQ/snarkVM/blob/046a2964f75576b2c4afbab9aa9eabc43ceb6dc3/synthesizer/program/src/lib.rs#L192
226-
pub fn reserved_keywords() -> impl Iterator<Item = &'static str> {
230+
pub fn aleo_reserved_keywords() -> impl Iterator<Item = &'static str> {
227231
use snarkvm::prelude::{Program, TestnetV0};
228232

229233
// Flatten RESTRICTED_KEYWORDS by ignoring ConsensusVersion
@@ -232,6 +236,14 @@ pub fn reserved_keywords() -> impl Iterator<Item = &'static str> {
232236
Program::<TestnetV0>::KEYWORDS.iter().copied().chain(restricted)
233237
}
234238

239+
fn is_leo_keyword(name: &str) -> bool {
240+
leo_parser_rowan::is_keyword(name)
241+
}
242+
243+
fn is_aleo_keyword(name: &str) -> bool {
244+
aleo_reserved_keywords().any(|kw| kw == name)
245+
}
246+
235247
/// Creates a configured ureq agent for Leo network requests.
236248
///
237249
/// Disables `http_status_as_error` so 4xx/5xx responses return `Ok(Response)`
@@ -351,3 +363,31 @@ pub fn filename_no_aleo_extension(path: &Path) -> Option<&str> {
351363
fn filename_no_extension<'a>(path: &'a Path, extension: &'static str) -> Option<&'a str> {
352364
path.file_name().and_then(|os_str| os_str.to_str()).and_then(|s| s.strip_suffix(extension))
353365
}
366+
367+
#[cfg(test)]
368+
mod tests {
369+
use super::{Package, is_valid_library_name, is_valid_program_name};
370+
371+
#[test]
372+
fn package_names_reject_leo_keywords() {
373+
assert!(!is_valid_program_name("in.aleo"));
374+
assert!(!is_valid_library_name("in"));
375+
}
376+
377+
#[test]
378+
fn package_names_accept_keyword_prefixes() {
379+
assert!(is_valid_program_name("inside.aleo"));
380+
assert!(is_valid_library_name("inside"));
381+
}
382+
383+
#[test]
384+
fn package_initialize_rejects_leo_keyword_program_names() {
385+
let dir = std::env::temp_dir().join(format!("leo_keyword_program_name_{}", std::process::id()));
386+
let _ = std::fs::remove_dir_all(&dir);
387+
std::fs::create_dir_all(&dir).unwrap();
388+
389+
assert!(Package::initialize("in", &dir, false).is_err());
390+
391+
std::fs::remove_dir_all(&dir).unwrap();
392+
}
393+
}

crates/parser-rowan/src/lexer.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,10 @@ fn ident_to_kind(s: &str) -> SyntaxKind {
344344
}
345345
}
346346

347+
pub(crate) fn is_keyword(s: &str) -> bool {
348+
ident_to_kind(s).is_keyword()
349+
}
350+
347351
/// Strip integer type suffix from a string, returning the numeric part.
348352
fn strip_int_suffix(s: &str) -> Option<&str> {
349353
// Check for integer type suffixes (longest first to match correctly)
@@ -724,6 +728,14 @@ mod tests {
724728
"#]]);
725729
}
726730

731+
#[test]
732+
fn keyword_predicate_matches_identifier_keywords() {
733+
assert!(super::is_keyword("view"));
734+
assert!(super::is_keyword("dyn"));
735+
assert!(super::is_keyword("in"));
736+
assert!(!super::is_keyword("inside"));
737+
}
738+
727739
#[test]
728740
fn lex_integers() {
729741
check_lex("123 0xFF 0b101 0o77", expect![[r#"

crates/parser-rowan/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ pub use parser::{
3737
pub use rowan::TextRange;
3838
pub use syntax_kind::{SyntaxKind, syntax_kind_from_raw};
3939

40+
/// Returns whether `s` is a Leo keyword recognized by the lexer.
41+
pub fn is_keyword(s: &str) -> bool {
42+
lexer::is_keyword(s)
43+
}
44+
4045
/// The Leo language type for rowan.
4146
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4247
pub enum LeoLanguage {}

crates/parser/src/rowan.rs

Lines changed: 3 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ impl<'a> ConversionContext<'a> {
259259
if text.starts_with('_') {
260260
self.handler.emit_err(crate::errors::identifier_cannot_start_with_underscore(ident.span));
261261
}
262-
if symbol_is_keyword(ident.name) {
262+
if leo_parser_rowan::is_keyword(&text) {
263263
self.emit_unexpected_str("an identifier", &text, ident.span);
264264
}
265265
}
@@ -3052,7 +3052,7 @@ pub fn parse_program(
30523052

30533053
if let Some(key) = compute_module_key(&module.name, root_dir.as_deref()) {
30543054
for segment in &key {
3055-
if symbol_is_keyword(*segment) {
3055+
if leo_parser_rowan::is_keyword(&segment.to_string()) {
30563056
return Err(crate::errors::keyword_used_as_module_name(key.iter().format("::"), segment).into());
30573057
}
30583058
}
@@ -3106,7 +3106,7 @@ pub fn parse_library(
31063106

31073107
if let Some(key) = compute_module_key(&module_sf.name, root_dir.as_deref()) {
31083108
for segment in &key {
3109-
if symbol_is_keyword(*segment) {
3109+
if leo_parser_rowan::is_keyword(&segment.to_string()) {
31103110
return Err(crate::errors::keyword_used_as_module_name(key.iter().format("::"), segment).into());
31113111
}
31123112
}
@@ -3266,62 +3266,6 @@ fn token_to_binary_op(kind: SyntaxKind) -> leo_ast::BinaryOperation {
32663266
}
32673267
}
32683268

3269-
fn symbol_is_keyword(symbol: Symbol) -> bool {
3270-
matches!(
3271-
symbol,
3272-
sym::address
3273-
| sym::aleo
3274-
| sym::As
3275-
| sym::assert
3276-
| sym::assert_eq
3277-
| sym::assert_neq
3278-
| sym::block
3279-
| sym::bool
3280-
| sym::Const
3281-
| sym::constant
3282-
| sym::constructor
3283-
| sym::Else
3284-
| sym::False
3285-
| sym::field
3286-
| sym::FnUpper
3287-
| sym::Fn
3288-
| sym::For
3289-
| sym::Final
3290-
| sym::group
3291-
| sym::i8
3292-
| sym::i16
3293-
| sym::i32
3294-
| sym::i64
3295-
| sym::i128
3296-
| sym::If
3297-
| sym::import
3298-
| sym::In
3299-
| sym::inline
3300-
| sym::Let
3301-
| sym::leo
3302-
| sym::mapping
3303-
| sym::storage
3304-
| sym::network
3305-
| sym::private
3306-
| sym::program
3307-
| sym::public
3308-
| sym::record
3309-
| sym::Return
3310-
| sym::scalar
3311-
| sym::script
3312-
| sym::SelfLower
3313-
| sym::signature
3314-
| sym::string
3315-
| sym::Struct
3316-
| sym::True
3317-
| sym::u8
3318-
| sym::u16
3319-
| sym::u32
3320-
| sym::u64
3321-
| sym::u128
3322-
)
3323-
}
3324-
33253269
/// Computes a module key from a `FileName`, optionally relative to a root directory.
33263270
fn compute_module_key(name: &FileName, root_dir: Option<&std::path::Path>) -> Option<Vec<Symbol>> {
33273271
let path = match name {

crates/parser/src/test.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,34 @@ fn runner_parser_test(test: &str) -> String {
188188
fn parser_tests() {
189189
leo_test_framework::run_tests("parser", runner_parser_test);
190190
}
191+
192+
#[test]
193+
#[serial]
194+
fn parser_rejects_keyword_module_names_from_rowan_lexer() {
195+
create_session_if_not_set_then(|_| {
196+
for keyword in ["view", "dyn"] {
197+
let buf = BufferEmitter::new();
198+
let handler = Handler::new(buf.clone());
199+
let source_file = with_session_globals(|s| {
200+
s.source_map.new_source("program test.aleo {}", FileName::Custom("test.leo".into()))
201+
});
202+
let module =
203+
with_session_globals(|s| s.source_map.new_source("", FileName::Custom(format!("{keyword}.leo"))));
204+
205+
let result = crate::parse_program(
206+
handler.clone(),
207+
&Default::default(),
208+
&source_file,
209+
&[module],
210+
NetworkName::TestnetV0,
211+
);
212+
213+
assert!(handler.extend_if_error(result).is_err(), "expected `{keyword}` module name to be rejected");
214+
let errors = format!("{}", buf.extract_errs());
215+
assert!(
216+
errors.contains(&format!("reserved keyword `{keyword}`")),
217+
"expected keyword module error for `{keyword}`"
218+
);
219+
}
220+
});
221+
}

0 commit comments

Comments
 (0)