use solidity_language_server::utils::{
byte_offset_to_position, is_valid_solidity_identifier, position_to_byte_offset,
};
use tower_lsp::lsp_types::Position;
#[test]
fn test_byte_offset_to_position_unix_newlines() {
let source = "line1\nline2\nline3\n";
assert_eq!(byte_offset_to_position(source, 0), Position::new(0, 0)); assert_eq!(byte_offset_to_position(source, 5), Position::new(0, 5)); assert_eq!(byte_offset_to_position(source, 6), Position::new(1, 0)); assert_eq!(byte_offset_to_position(source, 11), Position::new(1, 5)); assert_eq!(byte_offset_to_position(source, 12), Position::new(2, 0)); }
#[test]
fn test_byte_offset_to_position_windows_newlines() {
let source = "line1\r\nline2\r\nline3\r\n";
assert_eq!(byte_offset_to_position(source, 0), Position::new(0, 0));
assert_eq!(byte_offset_to_position(source, 5), Position::new(0, 5));
assert_eq!(byte_offset_to_position(source, 7), Position::new(1, 0)); assert_eq!(byte_offset_to_position(source, 12), Position::new(1, 5));
assert_eq!(byte_offset_to_position(source, 14), Position::new(2, 0));
}
#[test]
fn test_byte_offset_to_position_no_newlines() {
let source = "justoneline";
assert_eq!(byte_offset_to_position(source, 0), Position::new(0, 0));
assert_eq!(byte_offset_to_position(source, 5), Position::new(0, 5));
assert_eq!(byte_offset_to_position(source, 11), Position::new(0, 11));
}
#[test]
fn test_byte_offset_to_position_offset_out_of_bounds() {
let source = "short\nfile";
let offset = source.len() + 10;
assert_eq!(byte_offset_to_position(source, offset), Position::new(1, 4));
}
#[test]
fn test_byte_offset_to_position_empty_source() {
let source = "";
assert_eq!(byte_offset_to_position(source, 0), Position::new(0, 0));
assert_eq!(byte_offset_to_position(source, 10), Position::new(0, 0));
}
#[test]
fn test_position_to_byte_offset_basic() {
let source = "line1\nline2\nline3\n";
assert_eq!(position_to_byte_offset(source, Position::new(0, 0)), 0); assert_eq!(position_to_byte_offset(source, Position::new(0, 5)), 5); assert_eq!(position_to_byte_offset(source, Position::new(1, 0)), 6); assert_eq!(position_to_byte_offset(source, Position::new(1, 3)), 9); assert_eq!(position_to_byte_offset(source, Position::new(2, 0)), 12); }
#[test]
fn test_position_to_byte_offset_out_of_bounds() {
let source = "line1\nline2\n";
assert_eq!(
position_to_byte_offset(source, Position::new(10, 10)),
source.len()
);
}
#[test]
fn test_position_to_byte_offset_empty() {
let source = "";
assert_eq!(position_to_byte_offset(source, Position::new(0, 0)), 0);
}
#[test]
fn test_is_valid_solidity_identifier() {
assert!(is_valid_solidity_identifier("validName"));
assert!(is_valid_solidity_identifier("_valid"));
assert!(is_valid_solidity_identifier("a"));
assert!(is_valid_solidity_identifier("name123"));
assert!(is_valid_solidity_identifier("_"));
assert!(is_valid_solidity_identifier("a_b_c"));
assert!(is_valid_solidity_identifier("$token"));
assert!(is_valid_solidity_identifier("$"));
assert!(is_valid_solidity_identifier("my$var"));
assert!(!is_valid_solidity_identifier(""));
assert!(!is_valid_solidity_identifier("123invalid"));
assert!(!is_valid_solidity_identifier("invalid-name"));
assert!(!is_valid_solidity_identifier("invalid name"));
assert!(!is_valid_solidity_identifier("invalid.name"));
}
#[test]
fn test_rejects_active_keywords() {
let keywords = [
"abstract",
"address",
"anonymous",
"as",
"assembly",
"bool",
"break",
"bytes",
"calldata",
"catch",
"constant",
"constructor",
"continue",
"contract",
"delete",
"do",
"else",
"emit",
"enum",
"event",
"external",
"fallback",
"false",
"fixed",
"for",
"function",
"hex",
"if",
"immutable",
"import",
"indexed",
"interface",
"internal",
"is",
"library",
"mapping",
"memory",
"modifier",
"new",
"override",
"payable",
"pragma",
"private",
"public",
"pure",
"receive",
"return",
"returns",
"storage",
"string",
"struct",
"true",
"try",
"type",
"ufixed",
"unchecked",
"unicode",
"using",
"view",
"virtual",
"while",
];
for kw in keywords {
assert!(
!is_valid_solidity_identifier(kw),
"{kw:?} should be rejected"
);
}
}
#[test]
fn test_rejects_reserved_keywords() {
let reserved = [
"after",
"alias",
"apply",
"auto",
"byte",
"case",
"copyof",
"default",
"define",
"final",
"implements",
"in",
"inline",
"let",
"macro",
"match",
"mutable",
"null",
"of",
"partial",
"promise",
"reference",
"relocatable",
"sealed",
"sizeof",
"static",
"supports",
"switch",
"typedef",
"typeof",
"var",
];
for kw in reserved {
assert!(
!is_valid_solidity_identifier(kw),
"{kw:?} should be rejected"
);
}
}
#[test]
fn test_rejects_numeric_type_keywords() {
assert!(!is_valid_solidity_identifier("int"));
assert!(!is_valid_solidity_identifier("uint"));
for n in (8..=256).step_by(8) {
let int_kw = format!("int{n}");
let uint_kw = format!("uint{n}");
assert!(
!is_valid_solidity_identifier(&int_kw),
"{int_kw:?} should be rejected"
);
assert!(
!is_valid_solidity_identifier(&uint_kw),
"{uint_kw:?} should be rejected"
);
}
for n in 1..=32 {
let bytes_kw = format!("bytes{n}");
assert!(
!is_valid_solidity_identifier(&bytes_kw),
"{bytes_kw:?} should be rejected"
);
}
}
#[test]
fn test_allows_identifier_keywords() {
let allowed = [
"from",
"error",
"revert",
"global",
"transient",
"layout",
"at",
];
for kw in allowed {
assert!(
is_valid_solidity_identifier(kw),
"{kw:?} should be allowed as identifier"
);
}
}
#[test]
fn test_numeric_type_edge_cases() {
assert!(is_valid_solidity_identifier("int7"));
assert!(is_valid_solidity_identifier("int257"));
assert!(is_valid_solidity_identifier("int0"));
assert!(is_valid_solidity_identifier("uint3"));
assert!(is_valid_solidity_identifier("uint512"));
assert!(is_valid_solidity_identifier("bytes0"));
assert!(is_valid_solidity_identifier("bytes33"));
assert!(is_valid_solidity_identifier("bytes64"));
}
#[test]
fn test_byte_offset_to_position_utf16_bmp_chars() {
let source = "aâ–ˆb";
assert_eq!(source.len(), 5);
assert_eq!(byte_offset_to_position(source, 0), Position::new(0, 0)); assert_eq!(byte_offset_to_position(source, 1), Position::new(0, 1)); assert_eq!(byte_offset_to_position(source, 4), Position::new(0, 2)); assert_eq!(byte_offset_to_position(source, 5), Position::new(0, 3)); }
#[test]
fn test_byte_offset_to_position_utf16_surrogate_pair() {
let source = "a😀b";
assert_eq!(source.len(), 6);
assert_eq!(byte_offset_to_position(source, 0), Position::new(0, 0)); assert_eq!(byte_offset_to_position(source, 1), Position::new(0, 1)); assert_eq!(byte_offset_to_position(source, 5), Position::new(0, 3)); assert_eq!(byte_offset_to_position(source, 6), Position::new(0, 4)); }
#[test]
fn test_byte_offset_to_position_utf16_mixed_multibyte() {
let source = "é█😀x";
assert_eq!(source.len(), 10);
assert_eq!(byte_offset_to_position(source, 0), Position::new(0, 0)); assert_eq!(byte_offset_to_position(source, 2), Position::new(0, 1)); assert_eq!(byte_offset_to_position(source, 5), Position::new(0, 2)); assert_eq!(byte_offset_to_position(source, 9), Position::new(0, 4)); assert_eq!(byte_offset_to_position(source, 10), Position::new(0, 5)); }
#[test]
fn test_byte_offset_to_position_utf16_multiline() {
let source = "█░\na😀\nz";
assert_eq!(source.len(), 14);
assert_eq!(byte_offset_to_position(source, 0), Position::new(0, 0)); assert_eq!(byte_offset_to_position(source, 3), Position::new(0, 1)); assert_eq!(byte_offset_to_position(source, 6), Position::new(0, 2)); assert_eq!(byte_offset_to_position(source, 7), Position::new(1, 0)); assert_eq!(byte_offset_to_position(source, 8), Position::new(1, 1)); assert_eq!(byte_offset_to_position(source, 12), Position::new(1, 3)); assert_eq!(byte_offset_to_position(source, 13), Position::new(2, 0)); }
#[test]
fn test_position_to_byte_offset_utf16_bmp_chars() {
let source = "aâ–ˆb";
assert_eq!(position_to_byte_offset(source, Position::new(0, 0)), 0); assert_eq!(position_to_byte_offset(source, Position::new(0, 1)), 1); assert_eq!(position_to_byte_offset(source, Position::new(0, 2)), 4); }
#[test]
fn test_position_to_byte_offset_utf16_surrogate_pair() {
let source = "a😀b";
assert_eq!(position_to_byte_offset(source, Position::new(0, 0)), 0); assert_eq!(position_to_byte_offset(source, Position::new(0, 1)), 1); assert_eq!(position_to_byte_offset(source, Position::new(0, 3)), 5); }
#[test]
fn test_position_to_byte_offset_utf16_multiline() {
let source = "█░\na😀\nz";
assert_eq!(position_to_byte_offset(source, Position::new(0, 0)), 0);
assert_eq!(position_to_byte_offset(source, Position::new(0, 1)), 3);
assert_eq!(position_to_byte_offset(source, Position::new(1, 0)), 7);
assert_eq!(position_to_byte_offset(source, Position::new(1, 1)), 8);
assert_eq!(position_to_byte_offset(source, Position::new(1, 3)), 12);
assert_eq!(position_to_byte_offset(source, Position::new(2, 0)), 13);
}
#[test]
fn test_roundtrip_utf16_byte_to_pos_to_byte() {
let source = "ab😀é█cd\nef";
let char_boundaries: Vec<usize> = source.char_indices().map(|(i, _)| i).collect();
for &byte_off in &char_boundaries {
let pos = byte_offset_to_position(source, byte_off);
let recovered = position_to_byte_offset(source, pos);
assert_eq!(
recovered, byte_off,
"roundtrip failed for byte_off={byte_off}: pos=({},{}) -> {recovered}",
pos.line, pos.character
);
}
}