mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-18 22:13:39 +00:00
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:
1 parent
18cee10fa9
commit
6d6f55308f
14 files changed
+606
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
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"
|
||||
Reference in new issue
Block a user