use tower_lsp::lsp_types::{DocumentHighlight, DocumentHighlightKind, Position, Range};
use tree_sitter::{Node, Parser};
pub fn document_highlights(source: &str, position: Position) -> Vec<DocumentHighlight> {
let tree = match parse(source) {
Some(t) => t,
None => return vec![],
};
let root = tree.root_node();
let target = match find_identifier_at(root, source, position) {
Some(node) => node,
None => return vec![],
};
let name = &source[target.byte_range()];
let mut highlights = Vec::new();
collect_matching_identifiers(root, source, name, &mut highlights);
highlights
}
fn find_identifier_at<'a>(root: Node<'a>, _source: &str, position: Position) -> Option<Node<'a>> {
let point = tree_sitter::Point {
row: position.line as usize,
column: position.character as usize,
};
let node = root.descendant_for_point_range(point, point)?;
if node.kind() == "identifier" {
return Some(node);
}
let mut current = node;
for _ in 0..3 {
if current.kind() == "identifier" {
return Some(current);
}
current = current.parent()?;
}
let parent = node.parent()?;
let mut cursor = parent.walk();
parent
.children(&mut cursor)
.find(|child| child.kind() == "identifier" && contains_point(*child, point))
}
fn contains_point(node: Node, point: tree_sitter::Point) -> bool {
node.start_position() <= point && point <= node.end_position()
}
fn collect_matching_identifiers(
node: Node,
source: &str,
name: &str,
out: &mut Vec<DocumentHighlight>,
) {
if node.kind() == "identifier" && &source[node.byte_range()] == name {
let kind = classify_highlight(node, source);
out.push(DocumentHighlight {
range: range(node),
kind: Some(kind),
});
return; }
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_matching_identifiers(child, source, name, out);
}
}
fn classify_highlight(node: Node, _source: &str) -> DocumentHighlightKind {
let parent = match node.parent() {
Some(p) => p,
None => return DocumentHighlightKind::READ,
};
match parent.kind() {
"function_definition"
| "constructor_definition"
| "modifier_definition"
| "contract_declaration"
| "interface_declaration"
| "library_declaration"
| "struct_declaration"
| "enum_declaration"
| "event_definition"
| "error_declaration"
| "user_defined_type_definition"
| "state_variable_declaration"
| "struct_member" => {
if is_first_identifier(parent, node) {
return DocumentHighlightKind::WRITE;
}
return DocumentHighlightKind::READ;
}
"variable_declaration" => {
if is_first_identifier(parent, node) {
return DocumentHighlightKind::WRITE;
}
return DocumentHighlightKind::READ;
}
"parameter" | "event_parameter" | "error_parameter" => {
if is_first_identifier(parent, node) {
return DocumentHighlightKind::WRITE;
}
return DocumentHighlightKind::READ;
}
_ => {}
}
if parent.kind() == "expression"
&& let Some(grandparent) = parent.parent()
{
return classify_expression_context(grandparent, parent);
}
DocumentHighlightKind::READ
}
fn classify_expression_context(grandparent: Node, expr_node: Node) -> DocumentHighlightKind {
match grandparent.kind() {
"assignment_expression" => {
if is_lhs_of_assignment(grandparent, expr_node) {
DocumentHighlightKind::WRITE
} else {
DocumentHighlightKind::READ
}
}
"augmented_assignment_expression" => {
if is_lhs_of_assignment(grandparent, expr_node) {
DocumentHighlightKind::WRITE
} else {
DocumentHighlightKind::READ
}
}
"update_expression" => DocumentHighlightKind::WRITE,
"delete_expression" | "delete_statement" => DocumentHighlightKind::WRITE,
"tuple_expression" => {
if let Some(great_grandparent) = grandparent.parent()
&& let Some(ggp) = great_grandparent.parent()
&& (ggp.kind() == "assignment_expression"
|| ggp.kind() == "augmented_assignment_expression")
&& is_lhs_of_assignment(ggp, great_grandparent)
{
return DocumentHighlightKind::WRITE;
}
DocumentHighlightKind::READ
}
_ => DocumentHighlightKind::READ,
}
}
fn is_first_identifier(parent: Node, node: Node) -> bool {
let mut cursor = parent.walk();
for child in parent.children(&mut cursor) {
if child.kind() == "identifier" {
return child.id() == node.id();
}
}
false
}
fn is_lhs_of_assignment(assignment: Node, node: Node) -> bool {
let mut cursor = assignment.walk();
for child in assignment.children(&mut cursor) {
if child.is_named() {
return child.id() == node.id()
|| (child.start_byte() <= node.start_byte()
&& node.end_byte() <= child.end_byte());
}
}
false
}
fn parse(source: &str) -> Option<tree_sitter::Tree> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.expect("failed to load Solidity grammar");
parser.parse(source, None)
}
fn range(node: Node) -> Range {
let s = node.start_position();
let e = node.end_position();
Range {
start: Position::new(s.row as u32, s.column as u32),
end: Position::new(e.row as u32, e.column as u32),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn highlights_at(source: &str, line: u32, col: u32) -> Vec<(u32, u32, DocumentHighlightKind)> {
let result = document_highlights(source, Position::new(line, col));
result
.into_iter()
.map(|h| (h.range.start.line, h.range.start.character, h.kind.unwrap()))
.collect()
}
#[test]
fn test_empty_source() {
assert!(document_highlights("", Position::new(0, 0)).is_empty());
}
#[test]
fn test_no_identifier_at_position() {
let source = "pragma solidity ^0.8.0;";
let result = document_highlights(source, Position::new(0, 0));
let _ = result;
}
#[test]
fn test_state_variable_read_write() {
let source = r#"contract Foo {
uint256 public count;
function inc() public {
count += 1;
}
function get() public view returns (uint256) {
return count;
}
}"#;
let highlights = highlights_at(source, 1, 23);
assert!(
highlights.len() == 3,
"expected 3 highlights for 'count', got {}: {:?}",
highlights.len(),
highlights
);
let decl = highlights.iter().find(|h| h.0 == 1);
assert_eq!(
decl.map(|h| h.2),
Some(DocumentHighlightKind::WRITE),
"declaration should be Write"
);
let assign = highlights.iter().find(|h| h.0 == 3);
assert_eq!(
assign.map(|h| h.2),
Some(DocumentHighlightKind::WRITE),
"`count += 1` should be Write"
);
let read = highlights.iter().find(|h| h.0 == 6);
assert_eq!(
read.map(|h| h.2),
Some(DocumentHighlightKind::READ),
"`return count` should be Read"
);
}
#[test]
fn test_function_name_highlights() {
let source = r#"contract Foo {
function bar() public {}
function baz() public {
bar();
}
}"#;
let highlights = highlights_at(source, 1, 13);
assert_eq!(highlights.len(), 2, "expected 2 highlights for 'bar'");
assert_eq!(highlights[0].2, DocumentHighlightKind::WRITE);
assert_eq!(highlights[1].2, DocumentHighlightKind::READ);
}
#[test]
fn test_parameter_highlights() {
let source = r#"contract Foo {
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
}"#;
let highlights = highlights_at(source, 1, 25);
assert_eq!(highlights.len(), 2, "expected 2 highlights for 'a'");
assert_eq!(highlights[0].2, DocumentHighlightKind::WRITE);
assert_eq!(highlights[1].2, DocumentHighlightKind::READ);
}
#[test]
fn test_local_variable_highlights() {
let source = r#"contract Foo {
function bar() public {
uint256 x = 1;
uint256 y = x + 1;
x = y;
}
}"#;
let highlights = highlights_at(source, 2, 16);
assert_eq!(
highlights.len(),
3,
"expected 3 highlights for 'x': {:?}",
highlights
);
assert_eq!(highlights[0].2, DocumentHighlightKind::WRITE);
assert_eq!(highlights[1].2, DocumentHighlightKind::READ);
assert_eq!(highlights[2].2, DocumentHighlightKind::WRITE);
}
#[test]
fn test_contract_name_highlights() {
let source = r#"contract Foo {
Foo public self;
}"#;
let highlights = highlights_at(source, 0, 9);
assert!(
!highlights.is_empty(),
"expected at least 1 highlight for contract name 'Foo'"
);
assert_eq!(highlights[0].2, DocumentHighlightKind::WRITE);
}
#[test]
fn test_struct_name_and_members() {
let source = r#"contract Foo {
struct Info {
string name;
uint256 value;
}
Info public info;
}"#;
let highlights = highlights_at(source, 1, 11);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for 'Info'"
);
assert_eq!(highlights[0].2, DocumentHighlightKind::WRITE);
}
#[test]
fn test_event_name_highlights() {
let source = r#"contract Foo {
event Transfer(address from, address to, uint256 value);
function send() public {
emit Transfer(msg.sender, address(0), 100);
}
}"#;
let highlights = highlights_at(source, 1, 10);
assert_eq!(highlights.len(), 2, "expected 2 highlights for 'Transfer'");
assert_eq!(highlights[0].2, DocumentHighlightKind::WRITE);
assert_eq!(highlights[1].2, DocumentHighlightKind::READ);
}
#[test]
fn test_no_cross_name_pollution() {
let source = r#"contract Foo {
uint256 public x;
uint256 public y;
function bar() public {
x = y;
}
}"#;
let highlights = highlights_at(source, 1, 23);
for h in &highlights {
let text = source;
let line: &str = text.lines().nth(h.0 as usize).unwrap();
assert!(
line.contains("x"),
"highlight on line {} should contain 'x': '{}'",
h.0,
line
);
}
}
#[test]
fn test_enum_name_highlights() {
let source = r#"contract Foo {
enum Status { Active, Paused }
Status public status;
}"#;
let highlights = highlights_at(source, 1, 9);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for 'Status'"
);
assert_eq!(highlights[0].2, DocumentHighlightKind::WRITE);
}
#[test]
fn test_modifier_name_highlights() {
let source = r#"contract Foo {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function bar() public onlyOwner {}
}"#;
let highlights = highlights_at(source, 2, 13);
assert_eq!(highlights.len(), 2, "expected 2 highlights for 'onlyOwner'");
assert_eq!(highlights[0].2, DocumentHighlightKind::WRITE);
assert_eq!(highlights[1].2, DocumentHighlightKind::READ);
}
#[test]
fn test_shop_sol() {
let source = std::fs::read_to_string("example/Shop.sol").unwrap();
let highlights = document_highlights(&source, Position::new(68, 22));
assert!(
highlights.len() >= 2,
"Shop.sol 'PRICE' should have at least 2 highlights, got {}",
highlights.len()
);
let decl = highlights.iter().find(|h| h.range.start.line == 68);
assert_eq!(
decl.map(|h| h.kind),
Some(Some(DocumentHighlightKind::WRITE))
);
}
#[test]
fn test_increment_is_write() {
let source = r#"contract Foo {
uint256 public x;
function inc() public {
x++;
}
}"#;
let highlights = highlights_at(source, 1, 19);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for 'x', got {}: {:?}",
highlights.len(),
highlights
);
let inc = highlights.iter().find(|h| h.0 == 3);
assert_eq!(
inc.map(|h| h.2),
Some(DocumentHighlightKind::WRITE),
"`x++` should be Write, all highlights: {:?}",
highlights
);
}
#[test]
fn test_cursor_on_usage_finds_all() {
let source = r#"contract Foo {
uint256 public count;
function inc() public {
count += 1;
}
}"#;
let highlights_from_usage = highlights_at(source, 3, 8);
let highlights_from_decl = highlights_at(source, 1, 23);
assert_eq!(
highlights_from_usage.len(),
highlights_from_decl.len(),
"clicking on usage vs declaration should find the same set"
);
}
}