mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 16:18:03 +00:00
Add Fortran language support via fortls (#698)
This commit is contained in:
1 parent
45407f7a2a
commit
a8f6ad92cc
11 files changed
+1112
-404
No files matched your search
@@ -5,6 +5,8 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* Added `RenameSymbolTool` for renaming symbols across the codebase (if LS supports this operation).
|
||||
|
||||
* Language support:
|
||||
|
||||
* **Add support for Fortran** via fortls language server (requires `pip install fortls`)
|
||||
* **Add support for Julia** via LanguageServer.jl
|
||||
* **Add support for Haskell** via Haskell Language Server (HLS) with automatic discovery via ghcup, stack, or system PATH; supports both Stack and Cabal projects
|
||||
* **Add support for Scala** via Metals language server (requires some [manual setup](docs/scala_setup_guide_for_serena.md))
|
||||
|
||||
@@ -99,6 +99,7 @@ With Serena, we provide direct, out-of-the-box support for:
|
||||
* Scala (requires some [manual setup](docs/scala_setup_guide_for_serena.md); uses Metals LSP)
|
||||
* Erlang (requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls), experimental, might be slow or hang)
|
||||
* Perl (requires installation of Perl::LanguageServer)
|
||||
* Fortran (requires installation of fortls: `pip install fortls`)
|
||||
* Haskell (automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects)
|
||||
* Julia
|
||||
* AL
|
||||
|
||||
@@ -16,6 +16,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"requests>=2.32.3,<3",
|
||||
"pyright>=1.1.396,<2",
|
||||
"fortls>=3.2.2",
|
||||
"overrides>=7.7.0,<8",
|
||||
"python-dotenv>=1.0.0, <2",
|
||||
"mcp==1.12.3",
|
||||
@@ -281,6 +282,7 @@ markers = [
|
||||
"rego: language server running for Rego",
|
||||
"markdown: language server running for Markdown",
|
||||
"julia: Julia language server tests",
|
||||
"fortran: language server running for Fortran",
|
||||
"haskell: Haskell language server tests",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
Fortran Language Server implementation using fortls.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
class FortranLanguageServer(SolidLanguageServer):
|
||||
"""Fortran Language Server implementation using fortls."""
|
||||
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 3.0 # fortls needs time for workspace indexing
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
# For Fortran projects, ignore common build directories
|
||||
return super().is_ignored_dirname(dirname) or dirname in [
|
||||
"build",
|
||||
"Build",
|
||||
"BUILD",
|
||||
"bin",
|
||||
"lib",
|
||||
"mod", # Module files directory
|
||||
"obj", # Object files directory
|
||||
".cmake",
|
||||
"CMakeFiles",
|
||||
]
|
||||
|
||||
def _fix_fortls_selection_range(
|
||||
self, symbol: ls_types.UnifiedSymbolInformation, file_content: str
|
||||
) -> ls_types.UnifiedSymbolInformation:
|
||||
"""
|
||||
Fix fortls's incorrect selectionRange that points to line start instead of identifier name.
|
||||
|
||||
fortls bug: selectionRange.start.character is 0 (line start) but should point to the
|
||||
function/subroutine/module/program name position. This breaks MCP server features that
|
||||
rely on the exact identifier position for finding references.
|
||||
|
||||
Args:
|
||||
symbol: The symbol with potentially incorrect selectionRange
|
||||
file_content: Full file content to parse the line
|
||||
|
||||
Returns:
|
||||
Symbol with corrected selectionRange pointing to the identifier name
|
||||
|
||||
"""
|
||||
if "selectionRange" not in symbol:
|
||||
return symbol
|
||||
|
||||
sel_range = symbol["selectionRange"]
|
||||
start_line = sel_range["start"]["line"]
|
||||
start_char = sel_range["start"]["character"]
|
||||
|
||||
# Split file content into lines
|
||||
lines = file_content.split("\n")
|
||||
if start_line >= len(lines):
|
||||
return symbol
|
||||
|
||||
line = lines[start_line]
|
||||
|
||||
# Fortran keywords that define named constructs
|
||||
# Match patterns:
|
||||
# Standard keywords: <keyword> <whitespace> <identifier_name>
|
||||
# " function add_numbers(a, b) result(sum)" -> keyword="function", name="add_numbers"
|
||||
# "subroutine print_result(value)" -> keyword="subroutine", name="print_result"
|
||||
# "module math_utils" -> keyword="module", name="math_utils"
|
||||
# "program test_program" -> keyword="program", name="test_program"
|
||||
# "interface distance" -> keyword="interface", name="distance"
|
||||
#
|
||||
# Type definitions (can have :: syntax):
|
||||
# "type point" -> keyword="type", name="point"
|
||||
# "type :: point" -> keyword="type", name="point"
|
||||
# "type, extends(base) :: derived" -> keyword="type", name="derived"
|
||||
#
|
||||
# Submodules (have parent module in parentheses):
|
||||
# "submodule (parent_mod) child_mod" -> keyword="submodule", name="child_mod"
|
||||
|
||||
# Try type pattern first (has complex syntax with optional comma and ::)
|
||||
type_pattern = r"^\s*type\s*(?:,.*?)?\s*(?:::)?\s*([a-zA-Z_]\w*)"
|
||||
match = re.match(type_pattern, line, re.IGNORECASE)
|
||||
|
||||
if match:
|
||||
# For type pattern, identifier is in group 1
|
||||
identifier_name = match.group(1)
|
||||
identifier_start = match.start(1)
|
||||
else:
|
||||
# Try standard keywords pattern
|
||||
standard_pattern = r"^\s*(function|subroutine|module|program|interface)\s+([a-zA-Z_]\w*)"
|
||||
match = re.match(standard_pattern, line, re.IGNORECASE)
|
||||
|
||||
if not match:
|
||||
# Try submodule pattern
|
||||
submodule_pattern = r"^\s*submodule\s*\([^)]+\)\s+([a-zA-Z_]\w*)"
|
||||
match = re.match(submodule_pattern, line, re.IGNORECASE)
|
||||
|
||||
if match:
|
||||
identifier_name = match.group(1)
|
||||
identifier_start = match.start(1)
|
||||
else:
|
||||
identifier_name = match.group(2)
|
||||
identifier_start = match.start(2)
|
||||
|
||||
if match:
|
||||
# Create corrected selectionRange
|
||||
new_sel_range = {
|
||||
"start": {"line": start_line, "character": identifier_start},
|
||||
"end": {"line": start_line, "character": identifier_start + len(identifier_name)},
|
||||
}
|
||||
|
||||
# Create modified symbol with corrected selectionRange
|
||||
corrected_symbol = symbol.copy()
|
||||
corrected_symbol["selectionRange"] = new_sel_range
|
||||
|
||||
self.logger.log(
|
||||
f"Fixed fortls selectionRange for {identifier_name}: char {start_char} -> {identifier_start}",
|
||||
logging.DEBUG,
|
||||
)
|
||||
|
||||
return corrected_symbol
|
||||
|
||||
# If no match, return symbol unchanged (e.g., for variables, which don't have this pattern)
|
||||
return symbol
|
||||
|
||||
@override
|
||||
def request_document_symbols(
|
||||
self, relative_file_path: str, include_body: bool = False
|
||||
) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
|
||||
"""
|
||||
Override to fix fortls's incorrect selectionRange bug.
|
||||
|
||||
fortls returns selectionRange pointing to line start (character 0) instead of the
|
||||
identifier name position. This breaks MCP server features that rely on exact positions.
|
||||
|
||||
This override:
|
||||
1. Gets symbols from fortls via parent implementation
|
||||
2. Parses each symbol's line to find the correct identifier position
|
||||
3. Fixes selectionRange for all symbols recursively
|
||||
4. Returns corrected symbols
|
||||
|
||||
"""
|
||||
# Get symbols from fortls (with incorrect selectionRange)
|
||||
all_symbols, root_symbols = super().request_document_symbols(relative_file_path, include_body)
|
||||
|
||||
# Get file content for parsing
|
||||
with self.open_file(relative_file_path) as file_data:
|
||||
file_content = file_data.contents
|
||||
|
||||
# Fix selectionRange recursively for all symbols
|
||||
def fix_symbol_and_children(symbol: ls_types.UnifiedSymbolInformation) -> ls_types.UnifiedSymbolInformation:
|
||||
# Fix this symbol's selectionRange
|
||||
fixed = self._fix_fortls_selection_range(symbol, file_content)
|
||||
|
||||
# Fix children recursively
|
||||
if fixed.get("children"):
|
||||
fixed["children"] = [fix_symbol_and_children(child) for child in fixed["children"]]
|
||||
|
||||
return fixed
|
||||
|
||||
# Apply fix to all symbols
|
||||
fixed_all_symbols = [fix_symbol_and_children(sym) for sym in all_symbols]
|
||||
fixed_root_symbols = [fix_symbol_and_children(sym) for sym in root_symbols]
|
||||
|
||||
return fixed_all_symbols, fixed_root_symbols
|
||||
|
||||
@staticmethod
|
||||
def _check_fortls_installation():
|
||||
"""Check if fortls is available."""
|
||||
fortls_path = shutil.which("fortls")
|
||||
if fortls_path is None:
|
||||
raise RuntimeError("fortls is not installed or not in PATH.\nInstall it with: pip install fortls")
|
||||
return fortls_path
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: LanguageServerConfig,
|
||||
logger: LanguageServerLogger,
|
||||
repository_root_path: str,
|
||||
solidlsp_settings: SolidLSPSettings,
|
||||
):
|
||||
# Check fortls installation
|
||||
fortls_path = self._check_fortls_installation()
|
||||
|
||||
# Command to start fortls language server
|
||||
# fortls uses stdio for LSP communication by default
|
||||
fortls_cmd = f"{fortls_path}"
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=fortls_cmd, cwd=repository_root_path),
|
||||
"fortran", # Language ID for LSP
|
||||
solidlsp_settings,
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""Initialize params for Fortran Language Server."""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"completion": {
|
||||
"dynamicRegistration": True,
|
||||
"completionItem": {
|
||||
"snippetSupport": True,
|
||||
"commitCharactersSupport": True,
|
||||
"documentationFormat": ["markdown", "plaintext"],
|
||||
"deprecatedSupport": True,
|
||||
"preselectSupport": True,
|
||||
},
|
||||
},
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rangeFormatting": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"symbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
},
|
||||
},
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""Start Fortran Language Server process."""
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"Fortran LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def register_capability_handler(params):
|
||||
return
|
||||
|
||||
# Register LSP message handlers
|
||||
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.logger.log("Starting Fortran Language Server (fortls) process", logging.INFO)
|
||||
self.server.start()
|
||||
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
self.logger.log(
|
||||
"Sending initialize request to Fortran Language Server",
|
||||
logging.INFO,
|
||||
)
|
||||
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
|
||||
# Verify server capabilities
|
||||
capabilities = init_response.get("capabilities", {})
|
||||
assert "textDocumentSync" in capabilities
|
||||
if "completionProvider" in capabilities:
|
||||
self.logger.log("Fortran LSP completion provider available", logging.INFO)
|
||||
if "definitionProvider" in capabilities:
|
||||
self.logger.log("Fortran LSP definition provider available", logging.INFO)
|
||||
if "referencesProvider" in capabilities:
|
||||
self.logger.log("Fortran LSP references provider available", logging.INFO)
|
||||
if "documentSymbolProvider" in capabilities:
|
||||
self.logger.log("Fortran LSP document symbol provider available", logging.INFO)
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
# Fortran Language Server is ready after initialization
|
||||
self.server_ready.set()
|
||||
self.logger.log("Fortran Language Server initialization complete", logging.INFO)
|
||||
@@ -58,6 +58,7 @@ class Language(str, Enum):
|
||||
REGO = "rego"
|
||||
SCALA = "scala"
|
||||
JULIA = "julia"
|
||||
FORTRAN = "fortran"
|
||||
HASKELL = "haskell"
|
||||
# Experimental or deprecated Language Servers
|
||||
TYPESCRIPT_VTS = "typescript_vts"
|
||||
@@ -159,6 +160,10 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.scala", "*.sbt")
|
||||
case self.JULIA:
|
||||
return FilenameMatcher("*.jl")
|
||||
case self.FORTRAN:
|
||||
return FilenameMatcher(
|
||||
"*.f90", "*.F90", "*.f95", "*.F95", "*.f03", "*.F03", "*.f08", "*.F08", "*.f", "*.F", "*.for", "*.FOR", "*.fpp", "*.FPP"
|
||||
)
|
||||
case self.HASKELL:
|
||||
return FilenameMatcher("*.hs", "*.lhs")
|
||||
case _:
|
||||
@@ -294,6 +299,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.julia_server import JuliaLanguageServer
|
||||
|
||||
return JuliaLanguageServer
|
||||
case self.FORTRAN:
|
||||
from solidlsp.language_servers.fortran_language_server import FortranLanguageServer
|
||||
|
||||
return FortranLanguageServer
|
||||
case self.HASKELL:
|
||||
from solidlsp.language_servers.haskell_language_server import HaskellLanguageServer
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
program test_program
|
||||
use math_utils
|
||||
implicit none
|
||||
real :: result
|
||||
|
||||
! Test addition
|
||||
result = add_numbers(5.0, 3.0)
|
||||
call print_result(result)
|
||||
|
||||
! Test multiplication
|
||||
result = multiply_numbers(4.0, 2.0)
|
||||
call print_result(result)
|
||||
|
||||
print *, "All tests completed"
|
||||
end program test_program
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
module geometry_types
|
||||
implicit none
|
||||
|
||||
! Simple type definition
|
||||
type Point2D
|
||||
real :: x, y
|
||||
end type Point2D
|
||||
|
||||
! Type with double colon syntax
|
||||
type :: Circle
|
||||
real :: radius
|
||||
type(Point2D) :: center
|
||||
end type Circle
|
||||
|
||||
! Type with extends (inheritance)
|
||||
type, extends(Point2D) :: Point3D
|
||||
real :: z
|
||||
end type Point3D
|
||||
|
||||
! Named interface
|
||||
interface distance
|
||||
module procedure distance_2d, distance_3d
|
||||
end interface distance
|
||||
|
||||
contains
|
||||
|
||||
function distance_2d(p1, p2) result(dist)
|
||||
type(Point2D), intent(in) :: p1, p2
|
||||
real :: dist
|
||||
dist = sqrt((p2%x - p1%x)**2 + (p2%y - p1%y)**2)
|
||||
end function distance_2d
|
||||
|
||||
function distance_3d(p1, p2) result(dist)
|
||||
type(Point3D), intent(in) :: p1, p2
|
||||
real :: dist
|
||||
dist = sqrt((p2%x - p1%x)**2 + (p2%y - p1%y)**2 + (p2%z - p1%z)**2)
|
||||
end function distance_3d
|
||||
|
||||
function circle_area(c) result(area)
|
||||
type(Circle), intent(in) :: c
|
||||
real :: area
|
||||
real, parameter :: pi = 3.14159265359
|
||||
area = pi * c%radius**2
|
||||
end function circle_area
|
||||
|
||||
end module geometry_types
|
||||
@@ -0,0 +1,22 @@
|
||||
module math_utils
|
||||
implicit none
|
||||
contains
|
||||
|
||||
function add_numbers(a, b) result(sum)
|
||||
real, intent(in) :: a, b
|
||||
real :: sum
|
||||
sum = a + b
|
||||
end function add_numbers
|
||||
|
||||
function multiply_numbers(x, y) result(product)
|
||||
real, intent(in) :: x, y
|
||||
real :: product
|
||||
product = x * y
|
||||
end function multiply_numbers
|
||||
|
||||
subroutine print_result(value)
|
||||
real, intent(in) :: value
|
||||
print *, "Result is:", value
|
||||
end subroutine print_result
|
||||
end module math_utils
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Fortran language server tests
|
||||
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Basic tests for Fortran language server integration.
|
||||
|
||||
These tests validate some low-level LSP functionality and high-level Serena APIs.
|
||||
Note: These tests require fortls to be installed: pip install fortls
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
# Mark all tests in this module as fortran tests
|
||||
pytestmark = pytest.mark.fortran
|
||||
|
||||
|
||||
class TestFortranLanguageServer:
|
||||
"""Test Fortran language server functionality."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
|
||||
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding symbols using request_full_symbol_tree."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
|
||||
# Verify program symbol
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "test_program"), "test_program not found in symbol tree"
|
||||
|
||||
# Verify module symbol
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "math_utils"), "math_utils module not found in symbol tree"
|
||||
|
||||
# Verify function symbols
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "add_numbers"), "add_numbers function not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "multiply_numbers"), "multiply_numbers function not found in symbol tree"
|
||||
|
||||
# Verify subroutine symbol
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "print_result"), "print_result subroutine not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
|
||||
def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that document symbols can be retrieved from Fortran files."""
|
||||
# Test main.f90 - should have a program symbol
|
||||
main_symbols, _ = language_server.request_document_symbols("main.f90")
|
||||
program_names = [s.get("name") for s in main_symbols]
|
||||
assert "test_program" in program_names, f"Program 'test_program' not found in main.f90. Found: {program_names}"
|
||||
|
||||
# Test modules/math_utils.f90 - should have module and function symbols
|
||||
module_symbols, _ = language_server.request_document_symbols("modules/math_utils.f90")
|
||||
all_names = [s.get("name") for s in module_symbols]
|
||||
assert "math_utils" in all_names, f"Module 'math_utils' not found. Found: {all_names}"
|
||||
assert "add_numbers" in all_names, f"Function 'add_numbers' not found. Found: {all_names}"
|
||||
assert "multiply_numbers" in all_names, f"Function 'multiply_numbers' not found. Found: {all_names}"
|
||||
assert "print_result" in all_names, f"Subroutine 'print_result' not found. Found: {all_names}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
|
||||
def test_find_references_cross_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references across files using low-level request_references.
|
||||
|
||||
This tests the LSP textDocument/references capability.
|
||||
"""
|
||||
file_path = "modules/math_utils.f90"
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
|
||||
# Find the add_numbers function
|
||||
add_numbers_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "add_numbers":
|
||||
add_numbers_symbol = sym
|
||||
break
|
||||
|
||||
assert add_numbers_symbol is not None, "Could not find 'add_numbers' function symbol in math_utils.f90"
|
||||
|
||||
# Use selectionRange to query for references
|
||||
# Note: FortranLanguageServer automatically fixes fortls's incorrect selectionRange
|
||||
sel_start = add_numbers_symbol["selectionRange"]["start"]
|
||||
|
||||
# Query from the function name position using corrected selectionRange
|
||||
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
||||
|
||||
# Should find references (usage in main.f90 + definition in math_utils.f90)
|
||||
assert len(refs) > 0, "Should find references to add_numbers function"
|
||||
|
||||
# Verify that main.f90 references the function
|
||||
main_refs = [ref for ref in refs if "main.f90" in ref.get("relativePath", "")]
|
||||
assert (
|
||||
len(main_refs) > 0
|
||||
), f"Expected to find reference in main.f90, but found references in: {[ref.get('relativePath') for ref in refs]}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
|
||||
def test_find_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding definition across files using request_definition."""
|
||||
# In main.f90, line 7 (0-indexed: line 6) contains: result = add_numbers(5.0, 3.0)
|
||||
# We want to find the definition of add_numbers in modules/math_utils.f90
|
||||
main_file = "main.f90"
|
||||
|
||||
# Position on 'add_numbers' usage (approximately column 13)
|
||||
definition_location_list = language_server.request_definition(main_file, 6, 13)
|
||||
|
||||
if not definition_location_list:
|
||||
pytest.skip("fortls does not support cross-file go-to-definition for this case")
|
||||
|
||||
assert len(definition_location_list) >= 1, "Should find at least one definition"
|
||||
definition_location = definition_location_list[0]
|
||||
|
||||
# The definition should be in modules/math_utils.f90
|
||||
assert "math_utils.f90" in definition_location.get(
|
||||
"uri", ""
|
||||
), f"Expected definition to be in math_utils.f90, but found in: {definition_location.get('uri')}"
|
||||
|
||||
# Verify the definition is around the correct line (line 4, 0-indexed)
|
||||
assert (
|
||||
definition_location["range"]["start"]["line"] == 4
|
||||
), f"Expected definition at line 4, but found at line {definition_location['range']['start']['line']}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
|
||||
def test_request_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding symbols that reference a function - Serena's high-level API.
|
||||
|
||||
This tests request_referencing_symbols which returns not just locations but also
|
||||
the containing symbols that have the references. This is different from
|
||||
test_find_references_cross_file which only returns locations.
|
||||
|
||||
Note: FortranLanguageServer automatically fixes fortls's incorrect selectionRange.
|
||||
"""
|
||||
# Get the add_numbers function symbol from math_utils.f90
|
||||
file_path = "modules/math_utils.f90"
|
||||
symbols, _ = language_server.request_document_symbols(file_path)
|
||||
|
||||
# Find the add_numbers function
|
||||
add_numbers_symbol = None
|
||||
for sym in symbols:
|
||||
if sym.get("name") == "add_numbers":
|
||||
add_numbers_symbol = sym
|
||||
break
|
||||
|
||||
assert add_numbers_symbol is not None, "Could not find 'add_numbers' function symbol"
|
||||
|
||||
# Use selectionRange to query for referencing symbols
|
||||
# FortranLanguageServer automatically corrects fortls's incorrect selectionRange
|
||||
sel_start = add_numbers_symbol["selectionRange"]["start"]
|
||||
referencing_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
|
||||
|
||||
# Should find referencing symbols (not just locations, but symbols containing the references)
|
||||
assert len(referencing_symbols) > 0, "Should find referencing symbols when querying from function name position"
|
||||
|
||||
# Extract the symbols from ReferenceInSymbol objects
|
||||
# This is what makes this test different from test_find_references_cross_file:
|
||||
# we're testing that we get back SYMBOLS (with name, kind, location) not just locations
|
||||
ref_symbols = [ref.symbol for ref in referencing_symbols]
|
||||
|
||||
# Verify we got valid symbol structures with all required fields
|
||||
for symbol in ref_symbols:
|
||||
assert "name" in symbol, f"Symbol should have a name: {symbol}"
|
||||
assert "kind" in symbol, f"Symbol should have a kind: {symbol}"
|
||||
# Each symbol should have location information
|
||||
assert "location" in symbol, f"Symbol should have location: {symbol}"
|
||||
|
||||
# Note: fortls may not return all cross-file references through request_referencing_symbols
|
||||
# because it depends on finding containing symbols for each reference. We verify that
|
||||
# the API works and returns valid symbols with proper structure.
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
|
||||
def test_request_defining_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding the defining symbol - Serena's high-level API.
|
||||
|
||||
This is similar to test_find_definition_cross_file but uses the high-level
|
||||
request_defining_symbol which returns a full symbol with metadata, not just a location.
|
||||
"""
|
||||
# In main.f90, line 7 (0-indexed: line 6) contains: result = add_numbers(5.0, 3.0)
|
||||
# We want to find the definition of add_numbers
|
||||
main_file = "main.f90"
|
||||
|
||||
# Get the position of add_numbers usage in main.f90
|
||||
# Position on 'add_numbers' (approximately column 13)
|
||||
defining_symbol = language_server.request_defining_symbol(main_file, 6, 13)
|
||||
|
||||
if defining_symbol is None:
|
||||
pytest.skip("fortls does not support cross-file go-to-definition for this case")
|
||||
|
||||
# Should find the add_numbers function with full symbol information
|
||||
assert defining_symbol.get("name") == "add_numbers", f"Expected to find 'add_numbers' but got '{defining_symbol.get('name')}'"
|
||||
|
||||
# Check if we have location information
|
||||
if "location" not in defining_symbol or "relativePath" not in defining_symbol["location"]:
|
||||
pytest.skip("fortls found the symbol but doesn't provide complete location information")
|
||||
|
||||
# The definition should be in modules/math_utils.f90
|
||||
defining_path = defining_symbol["location"]["relativePath"]
|
||||
assert "math_utils.f90" in defining_path, f"Expected definition to be in math_utils.f90, but found in: {defining_path}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
|
||||
def test_request_containing_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding the containing symbol for a position in the code."""
|
||||
# Test finding the containing symbol for a position inside the add_numbers function
|
||||
file_path = "modules/math_utils.f90"
|
||||
|
||||
# Line 8 (0-indexed: line 7) is inside the add_numbers function: "sum = a + b"
|
||||
containing_symbol = language_server.request_containing_symbol(file_path, 7, 10, include_body=False)
|
||||
|
||||
if containing_symbol is None:
|
||||
pytest.skip("fortls does not support request_containing_symbol or couldn't find the containing symbol")
|
||||
|
||||
# Should find the add_numbers function as the containing symbol
|
||||
assert (
|
||||
containing_symbol.get("name") == "add_numbers"
|
||||
), f"Expected containing symbol 'add_numbers', got '{containing_symbol.get('name')}'"
|
||||
|
||||
# Verify the symbol kind is Function
|
||||
assert (
|
||||
containing_symbol.get("kind") == SymbolKind.Function.value
|
||||
), f"Expected Function kind ({SymbolKind.Function.value}), got {containing_symbol.get('kind')}"
|
||||
|
||||
# Verify location information exists
|
||||
assert "location" in containing_symbol, "Containing symbol should have location information"
|
||||
location = containing_symbol["location"]
|
||||
assert "range" in location, "Location should contain range information"
|
||||
assert "start" in location["range"] and "end" in location["range"], "Range should have start and end positions"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
|
||||
def test_type_and_interface_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that type definitions and interfaces are properly recognized with corrected selectionRange.
|
||||
|
||||
This verifies that the regex pattern correctly handles:
|
||||
- Simple type definitions (type Name)
|
||||
- Type with double colon (type :: Name)
|
||||
- Type with extends (type, extends(Base) :: Derived)
|
||||
- Named interfaces
|
||||
|
||||
fortls returns these as SymbolKind.Class (11) for types and SymbolKind.Interface (5) for interfaces.
|
||||
"""
|
||||
file_path = "modules/geometry.f90"
|
||||
symbols, _ = language_server.request_document_symbols(file_path)
|
||||
|
||||
# Find type and interface symbols
|
||||
type_names = []
|
||||
interface_names = []
|
||||
for sym in symbols:
|
||||
if sym.get("kind") == SymbolKind.Class.value: # Type definitions
|
||||
type_names.append(sym.get("name"))
|
||||
elif sym.get("kind") == SymbolKind.Interface.value: # Interfaces
|
||||
interface_names.append(sym.get("name"))
|
||||
|
||||
# Verify type definitions are found
|
||||
assert "Point2D" in type_names, f"Simple type 'Point2D' not found. Found types: {type_names}"
|
||||
assert "Circle" in type_names, f"Type with :: syntax 'Circle' not found. Found types: {type_names}"
|
||||
assert "Point3D" in type_names, f"Type with extends 'Point3D' not found. Found types: {type_names}"
|
||||
|
||||
# Verify interface is found
|
||||
assert "distance" in interface_names, f"Interface 'distance' not found. Found interfaces: {interface_names}"
|
||||
|
||||
# Verify selectionRange is corrected for a type symbol
|
||||
point3d_symbol = None
|
||||
for sym in symbols:
|
||||
if sym.get("name") == "Point3D":
|
||||
point3d_symbol = sym
|
||||
break
|
||||
|
||||
assert point3d_symbol is not None, "Could not find 'Point3D' type symbol"
|
||||
|
||||
# Use corrected selectionRange to find references
|
||||
# This tests that the fix works for types (not just functions)
|
||||
sel_start = point3d_symbol["selectionRange"]["start"]
|
||||
|
||||
# Verify selectionRange points to identifier name, not line start
|
||||
# Line for "type, extends(Point2D) :: Point3D" has Point3D at position > 0
|
||||
assert (
|
||||
sel_start["character"] > 0
|
||||
), f"selectionRange should point to identifier, not line start. Got character: {sel_start['character']}"
|
||||
|
||||
# Test that we can find references using the corrected position
|
||||
_refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
||||
# refs might be empty if Point3D isn't used elsewhere, but the call should not fail
|
||||
# The important thing is that it doesn't error due to wrong character position
|
||||
Reference in new issue
Block a user