Files
wehub-resource-sync c6af9e284a
Tests / catch-all (windows-latest) (push) Has been cancelled
Tests / jvm (macos-latest) (push) Has been cancelled
Tests / jvm (ubuntu-latest) (push) Has been cancelled
Tests / jvm (windows-latest) (push) Has been cancelled
Tests / native (macos-latest) (push) Has been cancelled
Tests / native (ubuntu-latest) (push) Has been cancelled
Tests / native (windows-latest) (push) Has been cancelled
Tests / niche (ubuntu-latest) (push) Has been cancelled
Tests / other-langs (macos-latest) (push) Has been cancelled
Tests / other-langs (ubuntu-latest) (push) Has been cancelled
Tests / other-langs (windows-latest) (push) Has been cancelled
Tests / catch-all (macos-latest) (push) Has been cancelled
Tests / catch-all (ubuntu-latest) (push) Has been cancelled
Docs Build / build (push) Has been cancelled
Docs Build / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Build and Push Docker Images / build-and-push (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:38 +08:00

208 lines
11 KiB
Python

"""
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
from test.conftest import language_has_verified_implementation_support
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
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"
if language_has_verified_implementation_support(Language.SOLIDITY):
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SOLIDITY], indirect=True)
def test_find_implementations(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
pos = _find_identifier_position(repo_path / "contracts/interfaces/IERC20.sol", "transfer")
assert pos is not None, "Should find 'transfer' identifier in IERC20.sol"
implementations = language_server.request_implementation("contracts/interfaces/IERC20.sol", *pos)
assert implementations, "Expected Token.transfer to be returned as an implementation"
assert any("Token.sol" in implementation.get("relativePath", "") for implementation in implementations), (
f"Expected Token.transfer implementation, got: {implementations}"
)
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SOLIDITY], indirect=True)
def test_request_implementing_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
pos = _find_identifier_position(repo_path / "contracts/interfaces/IERC20.sol", "transfer")
assert pos is not None, "Should find 'transfer' identifier in IERC20.sol"
implementing_symbols = language_server.request_implementing_symbols("contracts/interfaces/IERC20.sol", *pos)
assert implementing_symbols, "Expected implementing symbols for IERC20.transfer"
assert any(
symbol.get("name") == "transfer" and "Token.sol" in symbol["location"].get("relativePath", "")
for symbol in implementing_symbols
), f"Expected Token.transfer symbol, got: {implementing_symbols}"
@pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
for s in all_symbols:
if has_malformed_name(s):
malformed_symbols.append(s)
if malformed_symbols:
pytest.fail(
f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}",
pytrace=False,
)