feat(solidlsp): add Solidity language server support (#1156)

Integrates the Nomic Foundation @nomicfoundation/solidity-language-server
as a new Language.SOLIDITY backend (experimental). Supports .sol files with
go-to-definition, find references, document symbols, hover, and diagnostics.
The server is automatically installed via npm on first use.
This commit is contained in:
Vinh Tran authored and GitHub committed 2026-03-13 13:14:09 +01:00
1 parent 18cee10fa9
commit 6d6f55308f
14 files changed
+606 -1

No files matched your search

+3
View File
@@ -232,6 +232,9 @@ tmp/
# Exception: Don't ignore Julia test repository lib directory (contains source code)
!/test/resources/repos/julia/test_repo/lib
# Exception: Don't ignore Solidity test repository lib directory (contains source code)
!/test/resources/repos/solidity/test_repo/contracts/lib
# Swift
/test/resources/repos/swift/test_repo/.build
/test/resources/repos/swift/test_repo/.swiftpm
+7
View File
@@ -2,6 +2,13 @@
Status of the `main` branch. Changes prior to the next official version change will appear here.
* New language support:
* Add Solidity language server support (`Language.SOLIDITY`) using the
Nomic Foundation `@nomicfoundation/solidity-language-server`. Automatically
installed via npm. Supports `.sol` files with go-to-definition, find references,
document symbols, hover, and diagnostics. Works best with a `foundry.toml` or
`hardhat.config.js` in the project root.
* General:
* Add monorepo/multi-language support
* Project configuration files (`project.yml`) can now define multiple languages.
+1 -1
View File
@@ -82,7 +82,7 @@ that implement the language server protocol (LSP).
The underlying language servers are typically open-source projects (like Serena) or at least freely available for use.
With Serena's LSP library, we provide **support for over 30 programming languages**, including
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
> [!IMPORTANT]
> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details.
@@ -91,6 +91,9 @@ Some languages require additional installations or setup steps, as noted.
(requires [rustup](https://rustup.rs/) - uses rust-analyzer from your toolchain)
* **Scala**
(requires some [manual setup](../03-special-guides/scala_setup_guide_for_serena); uses Metals LSP)
* **Solidity**
(experimental; requires Node.js and npm; automatically installs `@nomicfoundation/solidity-language-server`;
works best with a `foundry.toml` or `hardhat.config.js` in the project root)
* **Swift**
* **TypeScript**
* **Vue**
+1
View File
@@ -330,6 +330,7 @@ markers = [
"systemverilog: language server running for SystemVerilog (uses verible-verilog-ls)",
"hlsl: language server running for HLSL shaders (uses shader-language-server)",
"lean4: language server running for Lean 4",
"solidity: language server running for Solidity (uses @nomicfoundation/solidity-language-server)",
]
[tool.codespell]
@@ -0,0 +1,220 @@
"""
Provides Solidity-specific instantiation of the LanguageServer class using
the Nomic Foundation Solidity Language Server (@nomicfoundation/solidity-language-server).
"""
import glob
import logging
import os
import pathlib
import shutil
import threading
from time import sleep
from typing import Any
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class SolidityLanguageServer(SolidLanguageServer):
"""
Provides Solidity-specific instantiation of the LanguageServer class using
the Nomic Foundation Solidity Language Server (@nomicfoundation/solidity-language-server).
Supports go-to-definition, find references, document symbols, hover, and diagnostics.
Requires Node.js and npm to be installed.
"""
@staticmethod
def _determine_log_level(line: str) -> int:
"""Suppress known non-critical stderr output from the Solidity language server."""
line_lower = line.lower()
if any(
[
"telemetry" in line_lower,
"could not find" in line_lower and "hardhat" in line_lower,
"no workspaceroot" in line_lower,
]
):
return logging.DEBUG
return SolidLanguageServer._determine_log_level(line)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a SolidityLanguageServer instance. Not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
super().__init__(
config,
repository_root_path,
None,
"solidity",
solidlsp_settings,
)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""
Install @nomicfoundation/solidity-language-server via npm and return the
path to the solidity-language-server executable.
"""
is_node_installed = shutil.which("node") is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install Node.js and try again."
is_npm_installed = shutil.which("npm") is not None
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="solidity-language-server",
description="Nomic Foundation Solidity Language Server",
command="npm install --prefix ./ @nomicfoundation/solidity-language-server@0.8.4",
platform_id="any",
),
]
)
solidity_ls_dir = os.path.join(self._ls_resources_dir, "solidity-lsp")
solidity_executable_path = os.path.join(solidity_ls_dir, "node_modules", ".bin", "nomicfoundation-solidity-language-server")
if os.name == "nt":
solidity_executable_path += ".cmd"
if not os.path.exists(solidity_executable_path):
log.info(f"Solidity Language Server executable not found at {solidity_executable_path}. Installing...")
deps.install(solidity_ls_dir)
log.info("Solidity language server dependencies installed successfully.")
if not os.path.exists(solidity_executable_path):
raise FileNotFoundError(
f"nomicfoundation-solidity-language-server executable not found at {solidity_executable_path}. "
"Something went wrong with the installation."
)
return solidity_executable_path
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in {"artifacts", "cache", "typechain-types"}
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""Return LSP InitializeParams for the Solidity language server."""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
return { # type: ignore
"locale": "en",
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"capabilities": {
"textDocument": {
"synchronization": {
"dynamicRegistration": True,
"didSave": True,
},
"completion": {
"dynamicRegistration": True,
"completionItem": {"snippetSupport": True},
},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))}, # type: ignore[arg-type]
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"], # type: ignore[list-item]
},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"symbol": {"dynamicRegistration": True},
},
},
"initializationOptions": {},
}
def _get_wait_time_for_cross_file_referencing(self) -> float:
# Small buffer for any post-indexing analysis the LSP performs after file-indexed events.
return 3.0
def _start_server(self) -> None:
"""Start the Solidity language server and wait for project indexing to finish."""
def do_nothing(params: Any) -> None:
return
def register_capability_handler(params: Any) -> None:
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
# Count .sol files in the project to know when indexing is complete.
sol_files = glob.glob(os.path.join(self.repository_root_path, "**", "*.sol"), recursive=True)
expected_count = len(sol_files)
indexed_count = [0]
all_indexed = threading.Event()
def on_file_indexed(params: Any) -> None:
indexed_count[0] += 1
uri = (params or {}).get("uri", "")
log.debug(f"Solidity LSP: file indexed ({indexed_count[0]}/{expected_count}): {uri}")
if indexed_count[0] >= expected_count:
all_indexed.set()
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_notification("custom/file-indexed", on_file_indexed)
log.info("Starting Solidity language server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
log.debug("Sending initialize request to Solidity language server")
init_response = self.server.send.initialize(initialize_params)
log.debug(f"Received initialize response from Solidity server: {init_response}")
if "documentSymbolProvider" in init_response.get("capabilities", {}):
log.debug("Solidity server supports document symbols")
else:
log.warning("Solidity server does not report document symbol support")
self.server.notify.initialized({})
if expected_count > 0:
log.info(f"Waiting for Solidity LSP to index {expected_count} .sol file(s)…")
completed = all_indexed.wait(timeout=60)
if completed:
log.info(f"Solidity LSP indexing complete ({indexed_count[0]}/{expected_count} files indexed)")
else:
log.warning(
f"Solidity LSP indexing timed out ({indexed_count[0]}/{expected_count} files indexed). "
"Waiting additional 30s for slow environments (e.g., CI)."
)
sleep(30)
log.info(f"Additional wait complete ({indexed_count[0]}/{expected_count} files indexed)")
else:
log.info("No .sol files found; skipping indexing wait")
log.info("Solidity language server initialization complete")
+14
View File
@@ -122,6 +122,13 @@ class Language(str, Enum):
Supports .sv, .svh, .v, .vh files.
Automatically downloads verible binary.
"""
SOLIDITY = "solidity"
"""Solidity language server using the Nomic Foundation Solidity Language Server
(@nomicfoundation/solidity-language-server).
Supports .sol files. Provides go-to-definition, find references, document symbols,
hover, and diagnostics. Requires Node.js and npm.
Works best with a foundry.toml or hardhat.config.js in the project root.
"""
@classmethod
def iter_all(cls, include_experimental: bool = False) -> Iterable[Self]:
@@ -148,6 +155,7 @@ class Language(str, Enum):
self.TOML,
self.GROOVY,
self.CPP_CCLS,
self.SOLIDITY,
}
def __str__(self) -> str:
@@ -289,6 +297,8 @@ class Language(str, Enum):
)
case self.SYSTEMVERILOG:
return FilenameMatcher("*.sv", "*.svh", "*.v", "*.vh")
case self.SOLIDITY:
return FilenameMatcher("*.sol")
case _:
raise ValueError(f"Unhandled language: {self}")
@@ -492,6 +502,10 @@ class Language(str, Enum):
from solidlsp.language_servers.systemverilog_server import SystemVerilogLanguageServer
return SystemVerilogLanguageServer
case self.SOLIDITY:
from solidlsp.language_servers.solidity_language_server import SolidityLanguageServer
return SolidityLanguageServer
case _:
raise ValueError(f"Unhandled language: {self}")
@@ -0,0 +1,4 @@
out/
cache/
artifacts/
node_modules/
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./interfaces/IERC20.sol";
import "./lib/SafeMath.sol";
/// @title Token
/// @notice A simple ERC-20 token implementation used as a test fixture.
contract Token is IERC20 {
using SafeMath for uint256;
// -------------------------------------------------------------------------
// State variables
// -------------------------------------------------------------------------
string public name;
string public symbol;
uint8 public decimals;
uint256 private _totalSupply;
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
// -------------------------------------------------------------------------
// Errors
// -------------------------------------------------------------------------
/// @notice Thrown when transferring to the zero address.
error ZeroAddress();
/// @notice Thrown when the caller's balance is insufficient.
error InsufficientBalance(address account, uint256 required, uint256 available);
/// @notice Thrown when the allowance is insufficient.
error InsufficientAllowance(address spender, uint256 required, uint256 available);
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
/// @param _name Human-readable token name.
/// @param _symbol Token ticker symbol.
/// @param supply Initial supply minted to `msg.sender` (in whole tokens).
constructor(string memory _name, string memory _symbol, uint256 supply) {
name = _name;
symbol = _symbol;
decimals = 18;
_mint(msg.sender, supply * 10 ** decimals);
}
// -------------------------------------------------------------------------
// IERC20 view functions
// -------------------------------------------------------------------------
/// @inheritdoc IERC20
function totalSupply() external view override returns (uint256) {
return _totalSupply;
}
/// @inheritdoc IERC20
function balanceOf(address account) external view override returns (uint256) {
return _balances[account];
}
/// @inheritdoc IERC20
function allowance(address owner, address spender) external view override returns (uint256) {
return _allowances[owner][spender];
}
// -------------------------------------------------------------------------
// IERC20 mutating functions
// -------------------------------------------------------------------------
/// @inheritdoc IERC20
function transfer(address to, uint256 amount) external override returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
/// @inheritdoc IERC20
function approve(address spender, uint256 amount) external override returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
/// @inheritdoc IERC20
function transferFrom(address from, address to, uint256 amount) external override returns (bool) {
uint256 currentAllowance = _allowances[from][msg.sender];
if (currentAllowance < amount) {
revert InsufficientAllowance(msg.sender, amount, currentAllowance);
}
_approve(from, msg.sender, currentAllowance.sub(amount));
_transfer(from, to, amount);
return true;
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
function _transfer(address from, address to, uint256 amount) internal {
if (to == address(0)) revert ZeroAddress();
uint256 fromBalance = _balances[from];
if (fromBalance < amount) {
revert InsufficientBalance(from, amount, fromBalance);
}
_balances[from] = fromBalance.sub(amount);
_balances[to] = _balances[to].add(amount);
emit Transfer(from, to, amount);
}
function _approve(address owner, address spender, uint256 amount) internal {
if (owner == address(0) || spender == address(0)) revert ZeroAddress();
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
function _mint(address account, uint256 amount) internal {
if (account == address(0)) revert ZeroAddress();
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
}
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title IERC20
/// @notice Minimal ERC-20 interface used by the test token.
interface IERC20 {
/// @notice Emitted when tokens are transferred between accounts.
event Transfer(address indexed from, address indexed to, uint256 value);
/// @notice Emitted when an allowance is set via `approve`.
event Approval(address indexed owner, address indexed spender, uint256 value);
/// @notice Returns the total token supply.
function totalSupply() external view returns (uint256);
/// @notice Returns the token balance of `account`.
function balanceOf(address account) external view returns (uint256);
/// @notice Transfers `amount` tokens to `to`.
function transfer(address to, uint256 amount) external returns (bool);
/// @notice Returns the remaining allowance that `spender` has over `owner`'s tokens.
function allowance(address owner, address spender) external view returns (uint256);
/// @notice Sets `amount` as the allowance of `spender` over the caller's tokens.
function approve(address spender, uint256 amount) external returns (bool);
/// @notice Moves `amount` tokens from `from` to `to` using the allowance mechanism.
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title SafeMath
/// @notice Arithmetic helpers with overflow checks (illustrative — 0.8+ reverts natively).
library SafeMath {
/// @notice Returns the sum of `a` and `b`, reverting on overflow.
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
/// @notice Returns `a` minus `b`, reverting on underflow.
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction underflow");
return a - b;
}
/// @notice Returns the product of `a` and `b`, reverting on overflow.
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
return a * b;
}
/// @notice Returns the integer division of `a` by `b`, reverting on division by zero.
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0, "SafeMath: division by zero");
return a / b;
}
}
@@ -0,0 +1,5 @@
[profile.default]
src = "contracts"
out = "out"
libs = []
solc = "0.8.20"
View File
Whitespace-only changes.
@@ -0,0 +1,165 @@
"""
Basic integration tests for the Solidity language server.
Tests validate symbol detection and reference finding using the Solidity test repository,
which contains a simple ERC-20 Token contract, a SafeMath library, and an IERC20 interface.
"""
import re
from pathlib import Path
from typing import Optional
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
def _find_identifier_position(file_path: Path, symbol_name: str) -> Optional[tuple[int, int]]:
"""Return the (line, column) of the first occurrence of *symbol_name* as an identifier.
Scans the file for a word-boundary match of *symbol_name* so that the position
returned is the exact location of the identifier, regardless of what range the
language server reports for the surrounding symbol. Returns None if not found.
"""
pattern = re.compile(r"\b" + re.escape(symbol_name) + r"\b")
with file_path.open(encoding="utf-8") as fh:
for line_idx, line in enumerate(fh):
m = pattern.search(line)
if m:
return line_idx, m.start()
return None
@pytest.mark.solidity
class TestSolidityLanguageServerBasics:
"""Test basic functionality of the Solidity language server."""
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SOLIDITY], indirect=True)
def test_solidity_language_server_initialization(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that the Solidity language server starts and initializes correctly."""
assert language_server is not None
assert language_server.language == Language.SOLIDITY
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SOLIDITY], indirect=True)
def test_token_contract_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that document symbols are found in Token.sol.
Verifies contract, state variables, errors, events, and function symbols.
"""
all_symbols, root_symbols = language_server.request_document_symbols("contracts/Token.sol").get_all_symbols_and_roots()
assert all_symbols is not None, "Should return symbols for Token.sol"
assert len(all_symbols) > 0, f"Should find symbols in Token.sol, found {len(all_symbols)}"
symbol_names = [sym.get("name") for sym in all_symbols]
# Contract-level symbol
assert "Token" in symbol_names, "Should detect the Token contract"
# State variables
assert "name" in symbol_names, "Should detect the 'name' state variable"
assert "symbol" in symbol_names, "Should detect the 'symbol' state variable"
assert "decimals" in symbol_names, "Should detect the 'decimals' state variable"
# Custom errors
assert "ZeroAddress" in symbol_names, "Should detect the 'ZeroAddress' custom error"
assert "InsufficientBalance" in symbol_names, "Should detect the 'InsufficientBalance' custom error"
# Functions
assert "totalSupply" in symbol_names, "Should detect the 'totalSupply' function"
assert "balanceOf" in symbol_names, "Should detect the 'balanceOf' function"
assert "transfer" in symbol_names, "Should detect the 'transfer' function"
assert "approve" in symbol_names, "Should detect the 'approve' function"
assert "transferFrom" in symbol_names, "Should detect the 'transferFrom' function"
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SOLIDITY], indirect=True)
def test_interface_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that document symbols are found in IERC20.sol."""
all_symbols, root_symbols = language_server.request_document_symbols("contracts/interfaces/IERC20.sol").get_all_symbols_and_roots()
assert all_symbols is not None, "Should return symbols for IERC20.sol"
assert len(all_symbols) > 0, f"Should find symbols in IERC20.sol, found {len(all_symbols)}"
symbol_names = [sym.get("name") for sym in all_symbols]
# Interface
assert "IERC20" in symbol_names, "Should detect the IERC20 interface"
# Events
assert "Transfer" in symbol_names, "Should detect the Transfer event"
assert "Approval" in symbol_names, "Should detect the Approval event"
# View functions
assert "totalSupply" in symbol_names, "Should detect totalSupply"
assert "balanceOf" in symbol_names, "Should detect balanceOf"
assert "allowance" in symbol_names, "Should detect allowance"
# Mutating functions
assert "transfer" in symbol_names, "Should detect transfer"
assert "approve" in symbol_names, "Should detect approve"
assert "transferFrom" in symbol_names, "Should detect transferFrom"
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SOLIDITY], indirect=True)
def test_library_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that document symbols are found in SafeMath.sol."""
all_symbols, root_symbols = language_server.request_document_symbols("contracts/lib/SafeMath.sol").get_all_symbols_and_roots()
assert all_symbols is not None, "Should return symbols for SafeMath.sol"
assert len(all_symbols) > 0, f"Should find symbols in SafeMath.sol, found {len(all_symbols)}"
symbol_names = [sym.get("name") for sym in all_symbols]
# Library
assert "SafeMath" in symbol_names, "Should detect the SafeMath library"
# Library functions
assert "add" in symbol_names, "Should detect the 'add' function"
assert "sub" in symbol_names, "Should detect the 'sub' function"
assert "mul" in symbol_names, "Should detect the 'mul' function"
assert "div" in symbol_names, "Should detect the 'div' function"
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SOLIDITY], indirect=True)
def test_within_file_references(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding within-file references to the _transfer helper in Token.sol."""
# Use the file to find the exact identifier position: the Solidity LSP reports
# the symbol range starting at the preceding whitespace/comment block, not the
# function keyword, so we locate '_transfer' directly in the source.
pos = _find_identifier_position(repo_path / "contracts/Token.sol", "_transfer")
assert pos is not None, "Should find '_transfer' identifier in Token.sol"
definition_line, definition_char = pos
references = language_server.request_references("contracts/Token.sol", definition_line, definition_char)
assert references is not None, "Should return references for '_transfer'"
assert (
len(references) >= 2
), f"'_transfer' should have at least 2 references (callers), found {len(references)}" # called in transfer() and transferFrom()
ref_files = {ref.get("uri", "") for ref in references}
assert any("Token.sol" in uri for uri in ref_files), "References should include Token.sol"
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SOLIDITY], indirect=True)
def test_cross_file_references(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding cross-file references: IERC20.transfer implemented in Token.sol."""
# Use 'transfer' in the interface — Token.sol inherits IERC20 and overrides it,
# so the LSP resolves the implementation site in Token.sol as a cross-file reference.
pos = _find_identifier_position(repo_path / "contracts/interfaces/IERC20.sol", "transfer")
assert pos is not None, "Should find 'transfer' identifier in IERC20.sol"
definition_line, definition_char = pos
references = language_server.request_references("contracts/interfaces/IERC20.sol", definition_line, definition_char)
assert references is not None, "Should return cross-file references for IERC20.transfer"
assert len(references) >= 1, f"IERC20.transfer should be referenced at least once (in Token.sol), found {len(references)}"
ref_files = {ref.get("uri", "") for ref in references}
assert any("Token.sol" in uri for uri in ref_files), "IERC20.transfer references should include Token.sol"