mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 12:15:10 +00:00
Merge pull request #1433 from oraios/features/diagnostics_and_ls_extensions
Features: diagnostics and LS extensions
This commit is contained in:
165 files changed
+5328
-552
No files matched your search
@@ -235,10 +235,10 @@ jobs:
|
||||
- name: Set up Julia
|
||||
uses: julia-actions/setup-julia@v2
|
||||
with:
|
||||
version: '1.10'
|
||||
version: '1.12.6'
|
||||
- name: Install Julia LanguageServer
|
||||
shell: bash
|
||||
run: julia -e 'using Pkg; Pkg.add("LanguageServer")'
|
||||
run: julia -e 'using Pkg; Pkg.add(Pkg.PackageSpec(name="LanguageServer", version="5.0.0"))'
|
||||
- name: Setup Haskell toolchain
|
||||
if: runner.os != 'Windows'
|
||||
uses: haskell/ghcup-setup@v1
|
||||
|
||||
+3
-1
@@ -79,6 +79,8 @@ downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
!test/resources/repos/dart/test_repo/lib/
|
||||
!test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
@@ -271,4 +273,4 @@ cabal.project.local*
|
||||
zz-misc/
|
||||
vue-implementation/
|
||||
|
||||
news/news.json
|
||||
news/news.json
|
||||
@@ -31,6 +31,7 @@ project_name: "serena"
|
||||
languages:
|
||||
- python
|
||||
- typescript
|
||||
- clojure
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
+12
-1
@@ -8,10 +8,21 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
See updated [documentation on modes](https://oraios.github.io/serena/02-usage/050_configuration.html#modes).
|
||||
- Serena's default configuration now uses `interactive` and `editing` as `base_modes` instead of as `default_modes`.
|
||||
|
||||
* JetBrains:
|
||||
- Add new tools:
|
||||
- `jet_brains_list_inspections`: Lists available IDE inspections (akin to diagnostics), optionally filtered by language or group
|
||||
- `jet_brains_run_inspections`: Runs IDE inspections on a file and returns the results
|
||||
|
||||
* LSP Tools:
|
||||
- Add new tools:
|
||||
- `find_declaration`: Finds the declaration/definition of a symbol
|
||||
- `find_implementations`: Finds the implementations of an interface or abstract method
|
||||
- `get_diagnostics_for_file`: Retrieves diagnostics for a specific file (errors, warnings, etc.)
|
||||
- `get_diagnostics_for_symbol`: Retrieves diagnostics pertaining to a specific symbol
|
||||
|
||||
* Language Servers:
|
||||
- Java (`eclipse.jdt.ls`): Add upstream JDTLS mode for offline / restricted-network use. Setting both `jdtls_path` and `lombok_path` in `ls_specific_settings.java` makes Serena use an existing upstream JDTLS installation (e.g. `brew install jdtls`) and the system JDK 21+, skipping the ~500 MB vscode-java VSIX, Gradle, and IntelliCode downloads. New related setting `java_home` lets the user override the JDK used to launch JDTLS. Default behavior unchanged — the JDTLS workspace hash is preserved bit-for-bit for users on the default route, so existing project caches are reused without a one-time reindex; the launcher path is mixed into the hash only when `jdtls_path` is set, isolating upstream installations from the default workspace. #1415
|
||||
|
||||
|
||||
# v1.2.0 (2026-04-27)
|
||||
|
||||
* General:
|
||||
|
||||
@@ -145,9 +145,10 @@ without reading entire files.
|
||||
| find referencing symbols | yes | yes |
|
||||
| search in project dependencies | -- | yes |
|
||||
| type hierarchy | -- | yes |
|
||||
| find declaration | -- | yes |
|
||||
| find implementations | -- | yes |
|
||||
| find declaration | yes | yes |
|
||||
| find implementations | yes | yes |
|
||||
| query external projects | yes | yes |
|
||||
| diagnostics/inspections | yes | yes |
|
||||
|
||||
### Refactoring
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Demonstrates diagnostics tools and edit-tool diagnostic reporting on the Serena repo itself.
|
||||
|
||||
The script creates a temporary Python file inside this repository, introduces one warning,
|
||||
shows file and symbol diagnostics, then introduces another warning and verifies that the
|
||||
second edit reports only the newly introduced warning.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.project import Project
|
||||
from serena.tools import (
|
||||
CreateTextFileTool,
|
||||
EditingToolWithDiagnostics,
|
||||
GetDiagnosticsForFileTool,
|
||||
GetDiagnosticsForSymbolTool,
|
||||
ReplaceContentTool,
|
||||
)
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
SEPARATOR = "=" * 80
|
||||
REPO_PATH = Path(REPO_ROOT)
|
||||
EDIT_RESULT_PREFIX = "Edit introduced new warning-or-higher diagnostics: "
|
||||
|
||||
|
||||
def make_agent() -> SerenaAgent:
|
||||
"""Create an LSP-backed Serena agent for the Serena repository."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
|
||||
project = Project(
|
||||
project_root=str(REPO_PATH),
|
||||
project_config=ProjectConfig(
|
||||
project_name="demo_serena_repo",
|
||||
languages=[Language.PYTHON],
|
||||
ignored_paths=[],
|
||||
excluded_tools=[],
|
||||
read_only=False,
|
||||
ignore_all_files_in_gitignore=True,
|
||||
initial_prompt="",
|
||||
encoding="utf-8",
|
||||
),
|
||||
serena_config=serena_config,
|
||||
)
|
||||
serena_config.projects = [RegisteredProject.from_project_instance(project)]
|
||||
return SerenaAgent(project="demo_serena_repo", serena_config=serena_config)
|
||||
|
||||
|
||||
def print_section(title: str) -> None:
|
||||
"""Print a visibly separated section header."""
|
||||
print(f"\n{SEPARATOR}")
|
||||
print(title)
|
||||
print(SEPARATOR)
|
||||
|
||||
|
||||
def parse_json_result(result: str) -> object:
|
||||
"""Parse and pretty-print JSON tool output."""
|
||||
parsed = json.loads(result)
|
||||
pprint(parsed, width=200)
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_edit_diagnostics_result(result: str) -> dict:
|
||||
"""Extract the grouped diagnostics payload from an edit-tool result."""
|
||||
assert result.startswith(EDIT_RESULT_PREFIX), result
|
||||
return json.loads(result[len(EDIT_RESULT_PREFIX) :])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
EditingToolWithDiagnostics.ENABLE_DIAGNOSTICS = True
|
||||
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="serena_demo_", dir=REPO_PATH))
|
||||
temp_file = temp_dir / "demo_temp_diagnostics.py"
|
||||
relative_path = temp_file.relative_to(REPO_PATH).as_posix()
|
||||
|
||||
initial_content = """def demo_existing_issue() -> int:
|
||||
value = 1
|
||||
return value
|
||||
"""
|
||||
|
||||
agent = make_agent()
|
||||
|
||||
try:
|
||||
# letting the language server finish startup
|
||||
agent.execute_task(lambda: None)
|
||||
|
||||
create_text_file_tool = agent.get_tool(CreateTextFileTool)
|
||||
replace_content_tool = agent.get_tool(ReplaceContentTool)
|
||||
get_diagnostics_for_file_tool = agent.get_tool(GetDiagnosticsForFileTool)
|
||||
get_diagnostics_for_symbol_tool = agent.get_tool(GetDiagnosticsForSymbolTool)
|
||||
|
||||
# creating a clean temporary file
|
||||
print_section("Create Temporary File")
|
||||
create_result = agent.execute_task(lambda: create_text_file_tool.apply(relative_path=relative_path, content=initial_content))
|
||||
print(create_result)
|
||||
|
||||
# showing file diagnostics before introducing any warning
|
||||
print_section("Initial File Diagnostics")
|
||||
initial_diagnostics_result = agent.execute_task(
|
||||
lambda: get_diagnostics_for_file_tool.apply(relative_path=relative_path, min_severity=2)
|
||||
)
|
||||
initial_diagnostics = parse_json_result(initial_diagnostics_result)
|
||||
assert initial_diagnostics == {}, initial_diagnostics
|
||||
|
||||
# introducing the first warning
|
||||
print_section("First Edit Result")
|
||||
first_edit_result = agent.execute_task(
|
||||
lambda: replace_content_tool.apply(
|
||||
relative_path=relative_path,
|
||||
needle="value = 1",
|
||||
repl="value = missing_one",
|
||||
mode="literal",
|
||||
)
|
||||
)
|
||||
print(first_edit_result)
|
||||
first_edit_diagnostics = parse_edit_diagnostics_result(first_edit_result)
|
||||
pprint(first_edit_diagnostics, width=200)
|
||||
assert "missing_one" in json.dumps(first_edit_diagnostics), first_edit_diagnostics
|
||||
|
||||
# showing the file- and symbol-level diagnostics after the first warning
|
||||
print_section("File Diagnostics After First Edit")
|
||||
diagnostics_after_first_edit_result = agent.execute_task(
|
||||
lambda: get_diagnostics_for_file_tool.apply(relative_path=relative_path, min_severity=2)
|
||||
)
|
||||
diagnostics_after_first_edit = parse_json_result(diagnostics_after_first_edit_result)
|
||||
assert "missing_one" in json.dumps(diagnostics_after_first_edit), diagnostics_after_first_edit
|
||||
|
||||
print_section("Symbol Diagnostics After First Edit")
|
||||
symbol_diagnostics_result = agent.execute_task(
|
||||
lambda: get_diagnostics_for_symbol_tool.apply(
|
||||
name_path="demo_existing_issue",
|
||||
reference_file=relative_path,
|
||||
min_severity=2,
|
||||
)
|
||||
)
|
||||
symbol_diagnostics = parse_json_result(symbol_diagnostics_result)
|
||||
assert "missing_one" in json.dumps(symbol_diagnostics), symbol_diagnostics
|
||||
|
||||
# introducing a second warning while keeping the first one unchanged
|
||||
print_section("Second Edit Result")
|
||||
second_edit_result = agent.execute_task(
|
||||
lambda: replace_content_tool.apply(
|
||||
relative_path=relative_path,
|
||||
needle=" return value\n",
|
||||
repl=" other = missing_two\n return value + other\n",
|
||||
mode="literal",
|
||||
)
|
||||
)
|
||||
print(second_edit_result)
|
||||
second_edit_diagnostics = parse_edit_diagnostics_result(second_edit_result)
|
||||
pprint(second_edit_diagnostics, width=200)
|
||||
second_edit_json = json.dumps(second_edit_diagnostics)
|
||||
assert "missing_two" in second_edit_json, second_edit_diagnostics
|
||||
assert "missing_one" not in second_edit_json, second_edit_diagnostics
|
||||
print("\nVerified: the second edit result reports only the newly introduced warning.")
|
||||
|
||||
# showing the complete file diagnostics after both warnings exist
|
||||
print_section("File Diagnostics After Second Edit")
|
||||
diagnostics_after_second_edit_result = agent.execute_task(
|
||||
lambda: get_diagnostics_for_file_tool.apply(relative_path=relative_path, min_severity=2)
|
||||
)
|
||||
diagnostics_after_second_edit = parse_json_result(diagnostics_after_second_edit_result)
|
||||
diagnostics_after_second_edit_json = json.dumps(diagnostics_after_second_edit)
|
||||
assert "missing_one" in diagnostics_after_second_edit_json, diagnostics_after_second_edit
|
||||
assert "missing_two" in diagnostics_after_second_edit_json, diagnostics_after_second_edit
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
agent.shutdown()
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Demonstrates both defining-symbol tools on the Python test repository.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.project import Project
|
||||
from serena.tools import FindDeclarationTool
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
SEPARATOR = "=" * 80
|
||||
PYTHON_TEST_REPO = Path(REPO_ROOT) / "test" / "resources" / "repos" / "python" / "test_repo"
|
||||
SERVICES_FILE = Path("test_repo") / "services.py"
|
||||
|
||||
|
||||
def make_agent(project_root: Path, language: Language, project_name: str) -> SerenaAgent:
|
||||
"""Create an LSP-backed Serena agent for a single explicit project."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
|
||||
project = Project(
|
||||
project_root=str(project_root),
|
||||
project_config=ProjectConfig(
|
||||
project_name=project_name,
|
||||
languages=[language],
|
||||
ignored_paths=[],
|
||||
excluded_tools=[],
|
||||
read_only=False,
|
||||
ignore_all_files_in_gitignore=True,
|
||||
initial_prompt="",
|
||||
encoding="utf-8",
|
||||
),
|
||||
serena_config=serena_config,
|
||||
)
|
||||
serena_config.projects = [RegisteredProject.from_project_instance(project)]
|
||||
return SerenaAgent(project=project_name, serena_config=serena_config)
|
||||
|
||||
|
||||
def print_section(title: str) -> None:
|
||||
"""Print a visibly separated section header."""
|
||||
print(f"\n{SEPARATOR}")
|
||||
print(title)
|
||||
print(SEPARATOR)
|
||||
|
||||
|
||||
def find_identifier_occurrence_position(file_path: Path, identifier: str, occurrence_index: int = 0) -> tuple[int, int]:
|
||||
"""Find the 0-based position of an identifier occurrence in a file."""
|
||||
pattern = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
||||
current_occurrence_index = 0
|
||||
with file_path.open(encoding="utf-8") as f:
|
||||
for line_index, line in enumerate(f):
|
||||
for match in pattern.finditer(line):
|
||||
if current_occurrence_index == occurrence_index:
|
||||
return line_index, match.start()
|
||||
current_occurrence_index += 1
|
||||
raise ValueError(f"Could not find occurrence {occurrence_index} of {identifier!r} in {file_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = make_agent(PYTHON_TEST_REPO, Language.PYTHON, "demo_python_test_repo")
|
||||
|
||||
try:
|
||||
# letting the language server finish startup
|
||||
agent.execute_task(lambda: None)
|
||||
|
||||
relative_path = SERVICES_FILE.as_posix()
|
||||
services_abs_path = PYTHON_TEST_REPO / SERVICES_FILE
|
||||
|
||||
# resolving via regex over the full file
|
||||
find_by_regex_tool = agent.get_tool(FindDeclarationTool)
|
||||
regex_result = agent.execute_task(
|
||||
lambda: find_by_regex_tool.apply(
|
||||
regex=r"from \.models import Item, (User)",
|
||||
relative_path=relative_path,
|
||||
include_info=True,
|
||||
)
|
||||
)
|
||||
|
||||
print_section("FindDefiningSymbolTool (File Regex)")
|
||||
regex_symbol = json.loads(regex_result)
|
||||
pprint(regex_symbol, width=200)
|
||||
|
||||
# resolving via regex restricted to one containing symbol body
|
||||
contained_regex_result = agent.execute_task(
|
||||
lambda: find_by_regex_tool.apply(
|
||||
regex=r"=\s+(User)\(",
|
||||
relative_path=relative_path,
|
||||
containing_symbol_name_path="UserService/create_user",
|
||||
include_info=True,
|
||||
)
|
||||
)
|
||||
|
||||
print_section("FindDefiningSymbolTool (Contained Regex)")
|
||||
contained_regex_symbol = json.loads(contained_regex_result)
|
||||
pprint(contained_regex_symbol, width=200)
|
||||
|
||||
# validating the demonstrated result
|
||||
for symbol in [regex_symbol, contained_regex_symbol]:
|
||||
assert symbol is not None, "Expected a defining symbol result"
|
||||
assert symbol.get("relative_path") is not None
|
||||
assert "models.py" in symbol["relative_path"], symbol
|
||||
assert "User" in json.dumps(symbol), symbol
|
||||
print("\nVerified definition target: User in models.py")
|
||||
finally:
|
||||
agent.shutdown()
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Demonstrates FindImplementationsTool on the Go test repository.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.project import Project
|
||||
from serena.tools import FindImplementationsTool
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
SEPARATOR = "=" * 80
|
||||
GO_TEST_REPO = Path(REPO_ROOT) / "test" / "resources" / "repos" / "go" / "test_repo"
|
||||
|
||||
|
||||
def make_agent(project_root: Path, language: Language, project_name: str) -> SerenaAgent:
|
||||
"""Create an LSP-backed Serena agent for a single explicit project."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
|
||||
project = Project(
|
||||
project_root=str(project_root),
|
||||
project_config=ProjectConfig(
|
||||
project_name=project_name,
|
||||
languages=[language],
|
||||
ignored_paths=[],
|
||||
excluded_tools=[],
|
||||
read_only=False,
|
||||
ignore_all_files_in_gitignore=True,
|
||||
initial_prompt="",
|
||||
encoding="utf-8",
|
||||
),
|
||||
serena_config=serena_config,
|
||||
)
|
||||
serena_config.projects = [RegisteredProject.from_project_instance(project)]
|
||||
return SerenaAgent(project=project_name, serena_config=serena_config)
|
||||
|
||||
|
||||
def print_section(title: str) -> None:
|
||||
"""Print a visibly separated section header."""
|
||||
print(f"\n{SEPARATOR}")
|
||||
print(title)
|
||||
print(SEPARATOR)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = make_agent(GO_TEST_REPO, Language.GO, "demo_go_test_repo")
|
||||
|
||||
try:
|
||||
# letting the language server finish startup
|
||||
agent.execute_task(lambda: None)
|
||||
|
||||
# running the implementation lookup
|
||||
find_implementations_tool = agent.get_tool(FindImplementationsTool)
|
||||
result = agent.execute_task(
|
||||
lambda: find_implementations_tool.apply(
|
||||
name_path="Greeter/FormatGreeting",
|
||||
relative_path="main.go",
|
||||
include_info=True,
|
||||
)
|
||||
)
|
||||
|
||||
print_section("Find Implementations Result")
|
||||
implementations = json.loads(result)
|
||||
pprint(implementations, width=200)
|
||||
|
||||
# validating the demonstrated result
|
||||
assert any(implementation["name_path"] == "(ConsoleGreeter).FormatGreeting" for implementation in implementations), result
|
||||
print("\nVerified implementation target: (ConsoleGreeter).FormatGreeting")
|
||||
finally:
|
||||
agent.shutdown()
|
||||
@@ -8,14 +8,16 @@ from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.config.serena_config import LanguageBackend, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.tools import (
|
||||
FindFileTool,
|
||||
FindReferencingSymbolsTool,
|
||||
GetDiagnosticsForFileTool,
|
||||
JetBrainsFindSymbolTool,
|
||||
JetBrainsGetSymbolsOverviewTool,
|
||||
JetBrainsInlineSymbol,
|
||||
JetBrainsRunInspectionsTool,
|
||||
JetBrainsSafeDeleteTool,
|
||||
SearchForPatternTool,
|
||||
)
|
||||
@@ -23,6 +25,7 @@ from serena.tools import (
|
||||
if __name__ == "__main__":
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
# project = Path(REPO_ROOT).parent / "serena-jetbrains-plugin-copy"
|
||||
project = Path(REPO_ROOT)
|
||||
agent = SerenaAgent(project=str(project), serena_config=serena_config)
|
||||
@@ -35,12 +38,13 @@ if __name__ == "__main__":
|
||||
overview_tool = agent.get_tool(JetBrainsGetSymbolsOverviewTool)
|
||||
safe_delete_tool = agent.get_tool(JetBrainsSafeDeleteTool)
|
||||
inline_symbol = agent.get_tool(JetBrainsInlineSymbol)
|
||||
diagnostics_in_file_tool = agent.get_tool(GetDiagnosticsForFileTool)
|
||||
jb_inspections_tool = agent.get_tool(JetBrainsRunInspectionsTool)
|
||||
|
||||
result = agent.execute_task(
|
||||
lambda: overview_tool.apply(
|
||||
lambda: diagnostics_in_file_tool.apply(
|
||||
# name_path_pattern="SerenaAgent",
|
||||
relative_path="src/serena/agent.py",
|
||||
depth=2,
|
||||
relative_path="test/resources/repos/clojure/test_repo/src/test_app/diagnostics_sample.clj",
|
||||
# keep_definition=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -57,15 +57,21 @@ class CodeEditor(Generic[TSymbol], ABC):
|
||||
"""
|
||||
raise NotImplementedError("This method must be overridden for each subclass")
|
||||
|
||||
def read_file(self, relative_path: str) -> str:
|
||||
def read_file(self, relative_path: str, lines: tuple[int, int] | None = None) -> str:
|
||||
"""
|
||||
Reads the content of a file.
|
||||
|
||||
:param relative_path: the relative path of the file to read
|
||||
:param lines: tuple of (first_line, last_line) to read only a specific line range (0-based, inclusive)
|
||||
:return: the content of the file
|
||||
"""
|
||||
with self._open_file_context(relative_path) as file:
|
||||
return file.get_contents()
|
||||
contents = file.get_contents()
|
||||
if lines is None:
|
||||
return contents
|
||||
else:
|
||||
first_line, last_line = lines
|
||||
return TextUtils.get_text_in_lines_range(contents, first_line, last_line)
|
||||
|
||||
@contextmanager
|
||||
def edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
|
||||
|
||||
@@ -661,6 +661,54 @@ class JetBrainsPluginClient(ToStringMixin):
|
||||
}
|
||||
return self._make_request("POST", "/debugReplClose", request_data)
|
||||
|
||||
def run_inspections(
|
||||
self,
|
||||
relative_path: str,
|
||||
min_severity: str | None = None,
|
||||
inspection_names: list[str] | None = None,
|
||||
start_line: int | None = None,
|
||||
end_line: int | None = None,
|
||||
) -> jb.RunInspectionsResponse:
|
||||
"""
|
||||
Runs IDE inspections on the given file and returns the results.
|
||||
|
||||
:param relative_path: the relative path to the file to inspect
|
||||
:param min_severity: minimum severity level to include (e.g. "WARNING", "ERROR")
|
||||
:param inspection_names: optional list of specific inspection names to run
|
||||
:param start_line: optional start line to restrict the inspection range
|
||||
:param end_line: optional end line to restrict the inspection range
|
||||
"""
|
||||
request_data: dict[str, Any] = {
|
||||
"relativePath": relative_path,
|
||||
}
|
||||
if min_severity is not None:
|
||||
request_data["minSeverity"] = min_severity
|
||||
if inspection_names is not None:
|
||||
request_data["inspectionNames"] = inspection_names
|
||||
if start_line is not None:
|
||||
request_data["startLine"] = start_line
|
||||
if end_line is not None:
|
||||
request_data["endLine"] = end_line
|
||||
return cast(jb.RunInspectionsResponse, self._make_request("POST", "/runInspectionsOnFile", request_data))
|
||||
|
||||
def list_inspections(
|
||||
self,
|
||||
language: str | None = None,
|
||||
group_path_contains: str | None = None,
|
||||
) -> jb.ListInspectionsResponse:
|
||||
"""
|
||||
Lists available IDE inspections, optionally filtered by language and/or group path.
|
||||
|
||||
:param language: optional language to filter inspections by (e.g. "Java", "Python")
|
||||
:param group_path_contains: optional substring to filter inspection group paths
|
||||
"""
|
||||
request_data: dict[str, Any] = {}
|
||||
if language is not None:
|
||||
request_data["language"] = language
|
||||
if group_path_contains is not None:
|
||||
request_data["groupPathContains"] = group_path_contains
|
||||
return cast(jb.ListInspectionsResponse, self._make_request("POST", "/listInspections", request_data))
|
||||
|
||||
def close(self) -> None:
|
||||
self._session.close()
|
||||
|
||||
|
||||
@@ -89,3 +89,30 @@ class TypeHierarchyNodeDTO(TypedDict):
|
||||
class TypeHierarchyResponse(TypedDict):
|
||||
hierarchy: NotRequired[list[TypeHierarchyNodeDTO]]
|
||||
num_levels_not_included: NotRequired[int]
|
||||
|
||||
|
||||
class InspectionResultDTO(TypedDict):
|
||||
inspection_name: str
|
||||
severity: str
|
||||
message: str
|
||||
relative_path: str
|
||||
start_line: int
|
||||
end_line: int
|
||||
start_col: NotRequired[int]
|
||||
end_col: NotRequired[int]
|
||||
quick_fix_available: NotRequired[bool]
|
||||
|
||||
|
||||
class RunInspectionsResponse(TypedDict):
|
||||
inspections: list[InspectionResultDTO]
|
||||
|
||||
|
||||
class InspectionInfoDTO(TypedDict):
|
||||
name: str
|
||||
group_path: str
|
||||
language: NotRequired[str]
|
||||
description: NotRequired[str]
|
||||
|
||||
|
||||
class ListInspectionsResponse(TypedDict):
|
||||
inspections: list[InspectionInfoDTO]
|
||||
@@ -26,3 +26,5 @@ included_optional_tools:
|
||||
- jet_brains_safe_delete
|
||||
- jet_brains_debug
|
||||
- serena_info
|
||||
- jet_brains_run_inspections
|
||||
- jet_brains_list_inspections
|
||||
+266
-1
@@ -3,6 +3,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from dataclasses import asdict, dataclass
|
||||
from time import perf_counter
|
||||
@@ -11,7 +12,7 @@ from typing import Any, Generic, Literal, NotRequired, Self, TypedDict, TypeVar
|
||||
from sensai.util.string import ToStringMixin
|
||||
|
||||
import serena.jetbrains.jetbrains_types as jb
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp import SolidLanguageServer, ls_types
|
||||
from solidlsp.ls import LSPFileBuffer
|
||||
from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol
|
||||
from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation
|
||||
@@ -309,6 +310,12 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
end_line = end_pos["line"] if end_pos else None
|
||||
return start_line, end_line
|
||||
|
||||
def get_body_line_numbers_or_raise(self) -> tuple[int, int]:
|
||||
start_line, end_line = self.get_body_line_numbers()
|
||||
if start_line is None or end_line is None:
|
||||
raise ValueError(f"Body line numbers could not be determined for {self.get_name_path()}")
|
||||
return start_line, end_line
|
||||
|
||||
@property
|
||||
def line(self) -> int | None:
|
||||
"""
|
||||
@@ -869,6 +876,264 @@ class LanguageServerSymbolRetriever:
|
||||
|
||||
return [ReferenceInLanguageServerSymbol.from_lsp_reference(r) for r in references]
|
||||
|
||||
def find_implementing_symbols(
|
||||
self,
|
||||
name_path: str,
|
||||
relative_file_path: str,
|
||||
include_body: bool = False,
|
||||
include_kinds: Sequence[SymbolKind] | None = None,
|
||||
exclude_kinds: Sequence[SymbolKind] | None = None,
|
||||
) -> list[LanguageServerSymbol]:
|
||||
"""
|
||||
Find all symbols that implement the specified symbol, which is assumed to be unique.
|
||||
|
||||
:param name_path: the name path of the symbol to find implementations for. While this can be a matching pattern,
|
||||
it should usually be the full path to ensure uniqueness.
|
||||
:param relative_file_path: the relative path of the file in which the implemented symbol is defined.
|
||||
:param include_body: whether to include the body of all symbols in the result.
|
||||
:param include_kinds: which kinds of symbols to include in the result.
|
||||
:param exclude_kinds: which kinds of symbols to exclude from the result.
|
||||
"""
|
||||
symbol = self.find_unique(name_path, substring_matching=False, within_relative_path=relative_file_path)
|
||||
return self.find_implementing_symbols_by_location(
|
||||
symbol.location,
|
||||
include_body=include_body,
|
||||
include_kinds=include_kinds,
|
||||
exclude_kinds=exclude_kinds,
|
||||
)
|
||||
|
||||
def find_implementing_symbols_by_location(
|
||||
self,
|
||||
symbol_location: LanguageServerSymbolLocation,
|
||||
include_body: bool = False,
|
||||
include_kinds: Sequence[SymbolKind] | None = None,
|
||||
exclude_kinds: Sequence[SymbolKind] | None = None,
|
||||
) -> list[LanguageServerSymbol]:
|
||||
"""
|
||||
Find all symbols that implement the symbol at the given location.
|
||||
|
||||
:param symbol_location: the location of the symbol for which to find implementations.
|
||||
Does not need to include an end_line, as it is unused in the search.
|
||||
:param include_body: whether to include the body of all symbols in the result.
|
||||
:param include_kinds: an optional sequence of ints representing the LSP symbol kind.
|
||||
If provided, only symbols of the given kinds will be included in the result.
|
||||
:param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
|
||||
Takes precedence over include_kinds.
|
||||
:return: a list of symbols that implement the given symbol
|
||||
"""
|
||||
if not symbol_location.has_position_in_file():
|
||||
raise ValueError("Symbol location does not contain a valid position in a file")
|
||||
assert symbol_location.relative_path is not None
|
||||
assert symbol_location.line is not None
|
||||
assert symbol_location.column is not None
|
||||
lang_server = self.get_language_server(symbol_location.relative_path)
|
||||
implementing_symbols = lang_server.request_implementing_symbols(
|
||||
relative_file_path=symbol_location.relative_path,
|
||||
line=symbol_location.line,
|
||||
column=symbol_location.column,
|
||||
include_body=include_body,
|
||||
)
|
||||
|
||||
if include_kinds is not None:
|
||||
implementing_symbols = [s for s in implementing_symbols if s["kind"] in include_kinds]
|
||||
|
||||
if exclude_kinds is not None:
|
||||
implementing_symbols = [s for s in implementing_symbols if s["kind"] not in exclude_kinds]
|
||||
|
||||
return [LanguageServerSymbol(s) for s in implementing_symbols]
|
||||
|
||||
def find_declaration(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
column: int,
|
||||
include_body: bool = False,
|
||||
) -> LanguageServerSymbol | None:
|
||||
"""
|
||||
Find the declaration/definition of the symbol at the given file position.
|
||||
|
||||
:param relative_file_path: the relative path to the file in which the symbol usage occurs.
|
||||
:param line: the 0-based line number of the symbol usage.
|
||||
:param column: the 0-based column number of the symbol usage.
|
||||
:param include_body: whether to include the body of the defining symbol in the result.
|
||||
:return: the defining symbol, or None if no definition could be resolved.
|
||||
"""
|
||||
lang_server = self.get_language_server(relative_file_path)
|
||||
defining_symbol = lang_server.request_defining_symbol(
|
||||
relative_file_path=relative_file_path,
|
||||
line=line,
|
||||
column=column,
|
||||
include_body=include_body,
|
||||
)
|
||||
if defining_symbol is None:
|
||||
return None
|
||||
return LanguageServerSymbol(defining_symbol)
|
||||
|
||||
def get_file_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
"""
|
||||
Get diagnostics for a file, optionally restricted to a line range and minimum severity.
|
||||
|
||||
:param relative_file_path: the relative path to the file.
|
||||
:param start_line: the first 0-based line to include.
|
||||
:param end_line: the last 0-based line to include. `-1` means until end of file.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
:return: the diagnostics matching the requested constraints.
|
||||
"""
|
||||
lang_server = self.get_language_server(relative_file_path)
|
||||
return lang_server.request_text_document_diagnostics(
|
||||
relative_file_path=relative_file_path,
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _symbol_identity(symbol: LanguageServerSymbol) -> tuple[str | None, int | None, int | None, str]:
|
||||
return (symbol.relative_path, symbol.line, symbol.column, symbol.get_name_path())
|
||||
|
||||
@staticmethod
|
||||
def _normalize_symbol_for_diagnostics(symbol: LanguageServerSymbol) -> LanguageServerSymbol:
|
||||
current_symbol = symbol
|
||||
while current_symbol.is_low_level():
|
||||
parent_symbol = current_symbol.get_parent()
|
||||
if parent_symbol is None:
|
||||
break
|
||||
current_symbol = parent_symbol
|
||||
return current_symbol
|
||||
|
||||
def find_diagnostic_owner_symbol(self, relative_file_path: str, line: int, column: int) -> LanguageServerSymbol | None:
|
||||
"""
|
||||
Find the symbol that should own a diagnostic at the given position.
|
||||
|
||||
This prefers the structural container of the diagnostic over low-level symbols such as
|
||||
local variables, because diagnostics are typically more meaningful when grouped by the
|
||||
surrounding function, method, class, or analogous construct.
|
||||
|
||||
:param relative_file_path: the relative path to the file containing the diagnostic.
|
||||
:param line: the 0-based line of the diagnostic.
|
||||
:param column: the 0-based column of the diagnostic.
|
||||
:return: the owning symbol, or None if no symbol could be resolved.
|
||||
"""
|
||||
lang_server = self.get_language_server(relative_file_path)
|
||||
symbol_dict = lang_server.request_symbol_at_location(
|
||||
relative_file_path=relative_file_path,
|
||||
line=line,
|
||||
column=column,
|
||||
)
|
||||
if symbol_dict is None:
|
||||
return None
|
||||
return self._normalize_symbol_for_diagnostics(LanguageServerSymbol(symbol_dict))
|
||||
|
||||
def _get_diagnostics_for_symbol(self, symbol: LanguageServerSymbol, min_severity: int) -> list[ls_types.Diagnostic]:
|
||||
relative_path = symbol.relative_path
|
||||
if relative_path is None:
|
||||
return []
|
||||
|
||||
start_line, end_line = symbol.get_body_line_numbers()
|
||||
if start_line is None:
|
||||
if symbol.line is None:
|
||||
return []
|
||||
start_line = symbol.line
|
||||
if end_line is None:
|
||||
end_line = start_line
|
||||
|
||||
return self.get_file_diagnostics(
|
||||
relative_file_path=relative_path,
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
def get_symbol_diagnostics(
|
||||
self,
|
||||
name_path: str,
|
||||
reference_file: str | None = None,
|
||||
check_symbol_references: bool = False,
|
||||
min_severity: int = 4,
|
||||
) -> dict[LanguageServerSymbol, list[ls_types.Diagnostic]]:
|
||||
"""
|
||||
Get diagnostics for the specified symbol and, optionally, for all symbols that reference it.
|
||||
|
||||
:param name_path: the name path of the symbol to find. It should usually be unique.
|
||||
:param reference_file: optional file path used to disambiguate the symbol search.
|
||||
:param check_symbol_references: whether to additionally collect diagnostics for referencing symbols.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
:return: a mapping from symbols to the diagnostics that overlap their body ranges.
|
||||
"""
|
||||
symbol = self.find_unique(name_path, substring_matching=False, within_relative_path=reference_file or None)
|
||||
return self.get_symbol_diagnostics_by_location(
|
||||
symbol.location,
|
||||
check_symbol_references=check_symbol_references,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
def get_symbol_diagnostics_by_location(
|
||||
self,
|
||||
symbol_location: LanguageServerSymbolLocation,
|
||||
check_symbol_references: bool = False,
|
||||
min_severity: int = 4,
|
||||
) -> dict[LanguageServerSymbol, list[ls_types.Diagnostic]]:
|
||||
"""
|
||||
Get diagnostics for the symbol at the given location and, optionally, for all referencing symbols.
|
||||
|
||||
:param symbol_location: location of the symbol to inspect.
|
||||
:param check_symbol_references: whether to additionally collect diagnostics for referencing symbols.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
:return: an ordered mapping from symbols to the diagnostics that overlap their body ranges.
|
||||
"""
|
||||
if not symbol_location.has_position_in_file():
|
||||
raise ValueError("Symbol location does not contain a valid position in a file")
|
||||
|
||||
symbol = self.find_by_location(symbol_location)
|
||||
if symbol is None:
|
||||
assert symbol_location.relative_path is not None
|
||||
assert symbol_location.line is not None
|
||||
assert symbol_location.column is not None
|
||||
lang_server = self.get_language_server(symbol_location.relative_path)
|
||||
symbol_dict = lang_server.request_symbol_at_location(
|
||||
relative_file_path=symbol_location.relative_path,
|
||||
line=symbol_location.line,
|
||||
column=symbol_location.column,
|
||||
)
|
||||
if symbol_dict is None:
|
||||
return {}
|
||||
symbol = LanguageServerSymbol(symbol_dict)
|
||||
|
||||
symbols_to_check: "OrderedDict[tuple[str | None, int | None, int | None, str], LanguageServerSymbol]" = OrderedDict()
|
||||
symbols_to_check[self._symbol_identity(symbol)] = symbol
|
||||
|
||||
if check_symbol_references:
|
||||
reference_symbols = self.find_referencing_symbols_by_location(
|
||||
symbol.location,
|
||||
include_body=False,
|
||||
exclude_kinds=[SymbolKind.File, SymbolKind.Module, SymbolKind.Package, SymbolKind.Namespace],
|
||||
)
|
||||
for reference in reference_symbols:
|
||||
reference_relative_path = reference.get_relative_path()
|
||||
normalized_reference_symbol = None
|
||||
if reference_relative_path is not None:
|
||||
normalized_reference_symbol = self.find_diagnostic_owner_symbol(
|
||||
relative_file_path=reference_relative_path,
|
||||
line=reference.line,
|
||||
column=reference.character,
|
||||
)
|
||||
if normalized_reference_symbol is None:
|
||||
normalized_reference_symbol = self._normalize_symbol_for_diagnostics(reference.symbol)
|
||||
symbols_to_check.setdefault(self._symbol_identity(normalized_reference_symbol), normalized_reference_symbol)
|
||||
|
||||
result: dict[LanguageServerSymbol, list[ls_types.Diagnostic]] = {}
|
||||
for current_symbol in symbols_to_check.values():
|
||||
diagnostics = self._get_diagnostics_for_symbol(current_symbol, min_severity=min_severity)
|
||||
if diagnostics:
|
||||
result[current_symbol] = diagnostics
|
||||
return result
|
||||
|
||||
def get_symbol_overview(self, relative_path: str) -> dict[str, list[LanguageServerSymbol]]:
|
||||
"""
|
||||
:param relative_path: the path of the file for which to get the symbol overview
|
||||
|
||||
@@ -12,7 +12,7 @@ from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from serena.tools import SUCCESS_RESULT, EditedFileContext, Tool, ToolMarkerCanEdit, ToolMarkerOptional
|
||||
from serena.tools import SUCCESS_RESULT, EditedFileContext, EditingToolWithDiagnostics, Tool, ToolMarkerOptional
|
||||
from serena.util.file_system import scan_directory
|
||||
from serena.util.text_utils import ContentReplacer, search_files
|
||||
|
||||
@@ -48,7 +48,7 @@ class ReadFileTool(Tool):
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class CreateTextFileTool(Tool, ToolMarkerCanEdit):
|
||||
class CreateTextFileTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Creates/overwrites a file in the project directory.
|
||||
"""
|
||||
@@ -61,23 +61,27 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit):
|
||||
:param content: the (appropriately encoded) content to write to the file
|
||||
:return: a message indicating success or failure
|
||||
"""
|
||||
project_root = self.get_project_root()
|
||||
abs_path = (Path(project_root) / relative_path).resolve()
|
||||
will_overwrite_existing = abs_path.exists()
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
# validating the destination path
|
||||
project_root = self.get_project_root()
|
||||
abs_path = (Path(project_root) / relative_path).resolve()
|
||||
will_overwrite_existing = abs_path.exists()
|
||||
|
||||
if will_overwrite_existing:
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=True)
|
||||
else:
|
||||
assert abs_path.is_relative_to(self.get_project_root()), (
|
||||
f"Cannot create file outside of the project directory, got {relative_path=}"
|
||||
)
|
||||
if will_overwrite_existing:
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=True)
|
||||
else:
|
||||
assert abs_path.is_relative_to(self.get_project_root()), (
|
||||
f"Cannot create file outside of the project directory, got {relative_path=}"
|
||||
)
|
||||
|
||||
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
abs_path.write_text(content, encoding=self.project.project_config.encoding, newline=self.project.line_ending.newline_str)
|
||||
answer = f"File created: {relative_path}."
|
||||
if will_overwrite_existing:
|
||||
answer += " Overwrote existing file."
|
||||
return answer
|
||||
# writing the file
|
||||
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
abs_path.write_text(content, encoding=self.project.project_config.encoding, newline=self.project.line_ending.newline_str)
|
||||
answer = f"File created: {relative_path}."
|
||||
if will_overwrite_existing:
|
||||
answer += " Overwrote existing file."
|
||||
|
||||
return diagnostics_context.format_result(answer)
|
||||
|
||||
|
||||
class ListDirTool(Tool):
|
||||
@@ -156,7 +160,7 @@ class FindFileTool(Tool):
|
||||
return result
|
||||
|
||||
|
||||
class ReplaceContentTool(Tool, ToolMarkerCanEdit):
|
||||
class ReplaceContentTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Replaces content in a file (optionally using regular expressions).
|
||||
"""
|
||||
@@ -212,16 +216,17 @@ class ReplaceContentTool(Tool, ToolMarkerCanEdit):
|
||||
Performs the replacement, with additional options not exposed in the tool.
|
||||
This function can be used internally by other tools.
|
||||
"""
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored)
|
||||
with EditedFileContext(relative_path, self.create_code_editor()) as context:
|
||||
original_content = context.get_original_content()
|
||||
replacer = ContentReplacer(mode=mode, allow_multiple_occurrences=allow_multiple_occurrences)
|
||||
updated_content = replacer.replace(original_content, needle, repl)
|
||||
context.set_updated_content(updated_content)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored)
|
||||
with EditedFileContext(relative_path, self.create_code_editor()) as context:
|
||||
original_content = context.get_original_content()
|
||||
replacer = ContentReplacer(mode=mode, allow_multiple_occurrences=allow_multiple_occurrences)
|
||||
updated_content = replacer.replace(original_content, needle, repl)
|
||||
context.set_updated_content(updated_content)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
class DeleteLinesTool(EditingToolWithDiagnostics, ToolMarkerOptional):
|
||||
"""
|
||||
Deletes a range of lines within a file.
|
||||
"""
|
||||
@@ -241,12 +246,13 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
:param start_line: the 0-based index of the first line to be deleted
|
||||
:param end_line: the 0-based index of the last line to be deleted
|
||||
"""
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.delete_lines(relative_path, start_line, end_line)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.delete_lines(relative_path, start_line, end_line)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
class ReplaceLinesTool(EditingToolWithDiagnostics, ToolMarkerOptional):
|
||||
"""
|
||||
Replaces a range of lines within a file with new content.
|
||||
"""
|
||||
@@ -268,16 +274,19 @@ class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
:param end_line: the 0-based index of the last line to be deleted
|
||||
:param content: the content to insert
|
||||
"""
|
||||
# normalizing the replacement content
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
result = self.agent.get_tool(DeleteLinesTool).apply(relative_path, start_line, end_line)
|
||||
if result != SUCCESS_RESULT:
|
||||
return result
|
||||
self.agent.get_tool(InsertAtLineTool).apply(relative_path, start_line, content)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.delete_lines(relative_path, start_line, end_line)
|
||||
code_editor.insert_at_line(relative_path, start_line, content)
|
||||
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
class InsertAtLineTool(EditingToolWithDiagnostics, ToolMarkerOptional):
|
||||
"""
|
||||
Inserts content at a given line in a file.
|
||||
"""
|
||||
@@ -298,11 +307,15 @@ class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
:param line: the 0-based index of the line to insert content at
|
||||
:param content: the content to be inserted
|
||||
"""
|
||||
# normalizing the inserted content
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_at_line(relative_path, line, content)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_at_line(relative_path, line, content)
|
||||
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class SearchForPatternTool(Tool):
|
||||
|
||||
@@ -595,3 +595,76 @@ class JetBrainsDebugTool(Tool, ToolMarkerOptional, ToolMarkerBeta):
|
||||
else:
|
||||
response = client.debug_close(repl_key=repl_key)
|
||||
return response.get("result", str(response))
|
||||
|
||||
|
||||
class JetBrainsRunInspectionsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Runs JetBrains IDE inspections on a file and returns the results.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
min_severity: str | None = None,
|
||||
inspection_names: list[str] | None = None,
|
||||
start_line: int | None = None,
|
||||
end_line: int | None = None,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Runs IDE inspections (code analysis) on the given file and returns the problems found.
|
||||
This leverages the full power of JetBrains' static analysis engine, including language-specific
|
||||
inspections, type checking, potential bugs, code style issues, and more.
|
||||
|
||||
:param relative_path: the relative path to the file to inspect.
|
||||
:param min_severity: minimum severity level to include in results (e.g. "ERROR", "WARNING", "WEAK_WARNING", "INFO").
|
||||
If not specified, all severities are returned.
|
||||
:param inspection_names: optional list of specific inspection names to run (e.g. ["UnusedImport", "TypeMismatch"]).
|
||||
If not specified, all applicable inspections are run.
|
||||
:param start_line: optional 1-based start line to restrict the inspection range.
|
||||
:param end_line: optional 1-based end line to restrict the inspection range.
|
||||
:param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned.
|
||||
-1 means the default value from the config will be used.
|
||||
:return: JSON string with inspection results including severity, message, and location.
|
||||
"""
|
||||
with JetBrainsPluginClient.from_project(self.project) as client:
|
||||
response_dict = client.run_inspections(
|
||||
relative_path=relative_path,
|
||||
min_severity=min_severity,
|
||||
inspection_names=inspection_names,
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
)
|
||||
result = self._to_json(response_dict)
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class JetBrainsListInspectionsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Lists available JetBrains IDE inspections, optionally filtered by language or group.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
language: str | None = None,
|
||||
group_path_contains: str | None = None,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Lists the available IDE inspections. Use this to discover which inspections can be passed
|
||||
to the run_inspections tool's `inspection_names` parameter.
|
||||
|
||||
:param language: optional language to filter by (e.g. "Java", "Python", "Kotlin").
|
||||
:param group_path_contains: optional substring to match against the inspection group path
|
||||
(e.g. "probable bugs", "code style").
|
||||
:param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned.
|
||||
-1 means the default value from the config will be used.
|
||||
:return: JSON string with the list of available inspections including name, group path, and language.
|
||||
"""
|
||||
with JetBrainsPluginClient.from_project(self.project) as client:
|
||||
response_dict = client.list_inspections(
|
||||
language=language,
|
||||
group_path_contains=group_path_contains,
|
||||
)
|
||||
result = self._to_json(response_dict)
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
@@ -6,15 +6,19 @@ import copy
|
||||
import os
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from serena.symbol import LanguageServerSymbol, LanguageServerSymbolDictGrouper
|
||||
from serena.tools import (
|
||||
SUCCESS_RESULT,
|
||||
EditingToolWithDiagnostics,
|
||||
Tool,
|
||||
ToolMarkerSymbolicEdit,
|
||||
ToolMarkerSymbolicRead,
|
||||
)
|
||||
from serena.tools.tools_base import ToolMarkerOptional
|
||||
from serena.util.ls_diagnostics import GroupedDiagnostics
|
||||
from serena.util.text_utils import find_text_coordinates
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
|
||||
|
||||
@@ -324,7 +328,242 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
|
||||
return self._limit_length(result_json, max_answer_chars, shortened_result_factories=shortened_results)
|
||||
|
||||
|
||||
class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit):
|
||||
class FindImplementationsTool(Tool, ToolMarkerSymbolicRead):
|
||||
"""
|
||||
Finds symbols that implement the given symbol using the language server backend.
|
||||
"""
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
relative_path: str,
|
||||
include_info: bool = False,
|
||||
include_kinds: list[int] = [], # noqa: B006
|
||||
exclude_kinds: list[int] = [], # noqa: B006
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Finds implementations of the symbol at the given `name_path`.
|
||||
|
||||
:param name_path: for finding the symbol to find implementations for, same logic as in the `find_symbol` tool.
|
||||
:param relative_path: the relative path to the file containing the symbol for which to find implementations.
|
||||
Note that here you can't pass a directory but must pass a file.
|
||||
:param include_info: whether to include additional info (hover-like, typically including docstring and signature),
|
||||
about the implementing symbols.
|
||||
:param include_kinds: same as in the `find_symbol` tool.
|
||||
:param exclude_kinds: same as in the `find_symbol` tool.
|
||||
:param max_answer_chars: same as in the `find_symbol` tool.
|
||||
:return: a list of JSON objects with the symbols implementing the requested symbol
|
||||
"""
|
||||
include_body = False
|
||||
parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None
|
||||
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
|
||||
implementing_symbols = symbol_retriever.find_implementing_symbols(
|
||||
name_path,
|
||||
relative_file_path=relative_path,
|
||||
include_body=include_body,
|
||||
include_kinds=parsed_include_kinds,
|
||||
exclude_kinds=parsed_exclude_kinds,
|
||||
)
|
||||
|
||||
symbol_dicts = [
|
||||
dict(s.to_dict(kind=True, relative_path=True, depth=0, body=include_body, body_location=True)) for s in implementing_symbols
|
||||
]
|
||||
if include_info:
|
||||
info_by_symbol = symbol_retriever.request_info_for_symbol_batch(implementing_symbols)
|
||||
for s, s_dict in zip(implementing_symbols, symbol_dicts, strict=True):
|
||||
if symbol_info := info_by_symbol.get(s):
|
||||
s_dict["info"] = symbol_info
|
||||
s_dict.pop("name", None) # name is included in the info
|
||||
|
||||
result = self._to_json(symbol_dicts)
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class FindDeclarationTool(Tool, ToolMarkerSymbolicRead):
|
||||
"""
|
||||
Finds the declaration/definition of a symbol
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
regex: str,
|
||||
containing_symbol_name_path: str | None = None,
|
||||
include_body: bool = False,
|
||||
include_info: bool = False,
|
||||
) -> str:
|
||||
r"""
|
||||
Finds the declaration of a symbol.
|
||||
|
||||
:param relative_path: the relative path to the source file containing the symbol for which to find the declaration.
|
||||
:param regex: a regular expression with one group, where the group matches the symbol for which to perform the lookup.
|
||||
For example, to find the declaration of the `process` method in a call like `obj.process()`,
|
||||
pass an expression like "obj\.(process)\(process_input_arg=37\)".
|
||||
Prefer regexes with sufficiently large context around the group to render the match unambiguous.
|
||||
Uses Python syntax with MULTILINE and DOTALL flags enabled.
|
||||
:param containing_symbol_name_path: optional name path of a containing symbol whose body shall be searched instead of the full file.
|
||||
:param include_body: whether to include the symbol's body in the result. Default False.
|
||||
:param include_info: whether to include additional info (hover-like). Default False.
|
||||
"""
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
relative_path = self._sanitize_input_param(relative_path)
|
||||
regex = self._sanitize_input_param(regex)
|
||||
|
||||
# find relevant location for lookup
|
||||
editor = self.create_code_editor()
|
||||
if not containing_symbol_name_path:
|
||||
content = editor.read_file(relative_path)
|
||||
coords = find_text_coordinates(content, regex, require_unique=True)
|
||||
assert coords is not None
|
||||
else:
|
||||
symbol = symbol_retriever.find_unique(name_path_pattern=containing_symbol_name_path, within_relative_path=relative_path)
|
||||
body_line_numers = symbol.get_body_line_numbers_or_raise()
|
||||
content = editor.read_file(relative_path, lines=body_line_numers)
|
||||
coords = find_text_coordinates(content, regex, require_unique=True)
|
||||
assert coords is not None
|
||||
coords.line += body_line_numers[0]
|
||||
|
||||
# retrieve declaration
|
||||
defining_symbol = symbol_retriever.find_declaration(
|
||||
relative_file_path=relative_path,
|
||||
line=coords.line,
|
||||
column=coords.col,
|
||||
include_body=include_body,
|
||||
)
|
||||
if defining_symbol is None:
|
||||
raise ValueError(
|
||||
f"No symbol declaration found at the location of the regex match. Location: {relative_path}:{coords.line}:{coords.col}."
|
||||
)
|
||||
|
||||
# create output
|
||||
symbol_dict = self._defining_symbol_to_result_dict(
|
||||
symbol_retriever,
|
||||
defining_symbol,
|
||||
include_body,
|
||||
include_info,
|
||||
)
|
||||
result = self._to_json(symbol_dict)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _defining_symbol_to_result_dict(
|
||||
symbol_retriever: Any,
|
||||
defining_symbol: LanguageServerSymbol,
|
||||
include_body: bool,
|
||||
include_info: bool,
|
||||
) -> dict[str, Any]:
|
||||
symbol_dict = dict(defining_symbol.to_dict(kind=True, relative_path=True, depth=0, body=include_body, body_location=True))
|
||||
if not include_body and include_info:
|
||||
if symbol_info := symbol_retriever.request_info_for_symbol(defining_symbol):
|
||||
symbol_dict["info"] = symbol_info
|
||||
symbol_dict.pop("name", None)
|
||||
return symbol_dict
|
||||
|
||||
|
||||
class GetDiagnosticsForFileTool(Tool, ToolMarkerSymbolicRead):
|
||||
"""
|
||||
Gets diagnostics for a file, optionally restricted to a line range, grouped by file, severity, and containing symbol.
|
||||
"""
|
||||
|
||||
FILE_LEVEL_DIAGNOSTIC_BUCKET = "<file>"
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Gets diagnostics for a file. Diagnostics are grouped as `relative_path -> severity -> name_path -> diagnostics_results`.
|
||||
If a diagnostic cannot be mapped to a symbol, it is grouped under the special name path `<file>`.
|
||||
|
||||
:param relative_path: the relative path to the file to inspect.
|
||||
:param start_line: the first 0-based line to include. Defaults to 0.
|
||||
:param end_line: the last 0-based line to include. Defaults to -1, which means until the end of the file.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
:param max_answer_chars: same as in the `find_symbol` tool.
|
||||
:return: grouped diagnostics for the requested file.
|
||||
"""
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
diagnostics = symbol_retriever.get_file_diagnostics(
|
||||
relative_file_path=relative_path,
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
grouped_diagnostics = GroupedDiagnostics()
|
||||
for diagnostic in diagnostics:
|
||||
diag_range = diagnostic["range"]["start"]
|
||||
name_path = self.FILE_LEVEL_DIAGNOSTIC_BUCKET
|
||||
owner_symbol = symbol_retriever.find_diagnostic_owner_symbol(
|
||||
relative_file_path=relative_path,
|
||||
line=diag_range["line"],
|
||||
column=diag_range["character"],
|
||||
)
|
||||
if owner_symbol is not None:
|
||||
name_path = owner_symbol.get_name_path()
|
||||
grouped_diagnostics.add(relative_path, name_path, diagnostic)
|
||||
|
||||
result = self._to_json(grouped_diagnostics.get_dict())
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class GetDiagnosticsForSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Gets diagnostics for a symbol and, optionally, for symbols that reference it.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
reference_file: str = "",
|
||||
check_symbol_references: bool = False,
|
||||
min_severity: int = 4,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Gets diagnostics for the specified symbol. When `check_symbol_references` is true, diagnostics for all
|
||||
referencing symbols are also included. The result is grouped as
|
||||
`relative_path -> severity -> name_path -> diagnostics_results`.
|
||||
|
||||
:param name_path: the name path of the symbol to inspect.
|
||||
:param reference_file: optional file path used to disambiguate the symbol search.
|
||||
:param check_symbol_references: whether to additionally collect diagnostics for symbols that reference the symbol.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
:param max_answer_chars: same as in the `find_symbol` tool.
|
||||
:return: grouped diagnostics for the requested symbol and, optionally, its referencing symbols.
|
||||
"""
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
diagnostics_by_symbol = symbol_retriever.get_symbol_diagnostics(
|
||||
name_path=name_path,
|
||||
reference_file=reference_file or None,
|
||||
check_symbol_references=check_symbol_references,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
grouped_diagnostics = GroupedDiagnostics()
|
||||
for symbol, diagnostics in diagnostics_by_symbol.items():
|
||||
relative_path = symbol.relative_path
|
||||
if relative_path is None:
|
||||
continue
|
||||
symbol_name_path = symbol.get_name_path()
|
||||
for diagnostic in diagnostics:
|
||||
grouped_diagnostics.add(relative_path, symbol_name_path, diagnostic)
|
||||
|
||||
result = self._to_json(grouped_diagnostics.get_dict())
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class ReplaceSymbolBodyTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Replaces the full definition of a symbol using the language server backend.
|
||||
"""
|
||||
@@ -348,16 +587,17 @@ class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit):
|
||||
in the programming language, including e.g. the signature line for functions.
|
||||
IMPORTANT: The body does NOT include any preceding docstrings/comments or imports, in particular.
|
||||
"""
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.replace_body(
|
||||
name_path,
|
||||
relative_file_path=relative_path,
|
||||
body=body,
|
||||
)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.replace_body(
|
||||
name_path,
|
||||
relative_file_path=relative_path,
|
||||
body=body,
|
||||
)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class InsertAfterSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
class InsertAfterSymbolTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Inserts content after the end of the definition of a given symbol.
|
||||
"""
|
||||
@@ -377,12 +617,13 @@ class InsertAfterSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
:param body: the body/content to be inserted. The inserted code shall begin with the next line after
|
||||
the symbol.
|
||||
"""
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_after_symbol(name_path, relative_file_path=relative_path, body=body)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_after_symbol(name_path, relative_file_path=relative_path, body=body)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
class InsertBeforeSymbolTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Inserts content before the beginning of the definition of a given symbol.
|
||||
"""
|
||||
@@ -402,9 +643,10 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
:param relative_path: the relative path to the file containing the symbol
|
||||
:param body: the body/content to be inserted before the line in which the referenced symbol is defined
|
||||
"""
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_before_symbol(name_path, relative_file_path=relative_path, body=body)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_before_symbol(name_path, relative_file_path=relative_path, body=body)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class RenameSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Protocol, Self, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, Optional, Protocol, Self, TypeVar, cast
|
||||
|
||||
from mcp import Implementation
|
||||
from mcp.server.fastmcp import Context
|
||||
@@ -18,6 +18,7 @@ from serena.project import MemoriesManager, Project
|
||||
from serena.prompt_factory import PromptFactory
|
||||
from serena.util.class_decorators import singleton
|
||||
from serena.util.inspection import iter_subclasses
|
||||
from serena.util.ls_diagnostics import DiagnosticsDiff, EditedFilePath, PublishedDiagnosticsSnapshot
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -319,7 +320,7 @@ class Tool(Component):
|
||||
if client_str != self.get_last_tool_call_client_str():
|
||||
log.debug(f"Updating client info: {client_info}")
|
||||
self.set_last_tool_call_client_str(client_str)
|
||||
except BaseException as e:
|
||||
except Exception as e:
|
||||
log.info(f"Failed to get client info: {e}.")
|
||||
|
||||
def task() -> str:
|
||||
@@ -403,6 +404,62 @@ class Tool(Component):
|
||||
return json.dumps(x, ensure_ascii=False)
|
||||
|
||||
|
||||
class EditingToolWithDiagnostics(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Base class for editing tools that want to capture and report changes in LSP diagnostics before and after the edit.
|
||||
"""
|
||||
|
||||
ENABLE_DIAGNOSTICS: bool = False
|
||||
"""
|
||||
Global flag to enable/disable diagnostics for LSP-based editing tools derived from this class.
|
||||
The feature is currently disabled, because per-edit diagnostics are a questionable feature, since individual
|
||||
edits often intentionally introduce diagnostics (e.g. function signature mismatches or even syntax errors) that
|
||||
are then resolved in subsequent edits.
|
||||
"""
|
||||
|
||||
DIAGNOSTICS_KEY = "diagnostics[warning-or-higher]"
|
||||
|
||||
class DiagnosticsContext:
|
||||
def __init__(self, tool: "EditingToolWithDiagnostics", *edited_relative_paths: str) -> None:
|
||||
self._tool = tool
|
||||
self._is_diagnostics_enabled = tool.ENABLE_DIAGNOSTICS and tool.agent.is_using_language_server()
|
||||
self._edited_files = [EditedFilePath(path, path) for path in edited_relative_paths]
|
||||
self._before_edit_diagnostics_snapshot: PublishedDiagnosticsSnapshot | None = None
|
||||
self._symbol_retriever: Optional["LanguageServerSymbolRetriever"] | None = None
|
||||
if self._is_diagnostics_enabled:
|
||||
self._symbol_retriever = tool.create_language_server_symbol_retriever()
|
||||
self._before_edit_diagnostics_snapshot = PublishedDiagnosticsSnapshot(self._edited_files, self._symbol_retriever)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
|
||||
pass
|
||||
|
||||
def format_result(
|
||||
self,
|
||||
base_result: str,
|
||||
) -> str:
|
||||
if not self._is_diagnostics_enabled:
|
||||
return base_result
|
||||
|
||||
if self._before_edit_diagnostics_snapshot is None:
|
||||
return base_result
|
||||
|
||||
assert self._symbol_retriever is not None
|
||||
diagnostics_diff = DiagnosticsDiff(self._before_edit_diagnostics_snapshot, self._edited_files, self._symbol_retriever)
|
||||
grouped_diagnostics = diagnostics_diff.get_grouped_diagnostics().get_dict()
|
||||
|
||||
if not grouped_diagnostics:
|
||||
return base_result
|
||||
else:
|
||||
result_dict = {
|
||||
"result": base_result,
|
||||
EditingToolWithDiagnostics.DIAGNOSTICS_KEY: grouped_diagnostics,
|
||||
}
|
||||
return self._tool._to_json(result_dict)
|
||||
|
||||
|
||||
class EditedFileContext:
|
||||
"""
|
||||
Context manager for file editing.
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticSeverity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.symbol import LanguageServerSymbolRetriever
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EditedFilePath:
|
||||
before_relative_path: str
|
||||
after_relative_path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DiagnosticIdentity:
|
||||
message: str
|
||||
start_line: int
|
||||
start_character: int
|
||||
end_line: int
|
||||
end_character: int
|
||||
severity: int | None
|
||||
code_repr: str | None
|
||||
source: str | None
|
||||
|
||||
@classmethod
|
||||
def from_diagnostic(cls, diagnostic: ls_types.Diagnostic) -> "DiagnosticIdentity":
|
||||
diagnostic_range = diagnostic["range"]
|
||||
start = diagnostic_range["start"]
|
||||
end = diagnostic_range["end"]
|
||||
return DiagnosticIdentity(
|
||||
message=diagnostic["message"],
|
||||
start_line=start["line"],
|
||||
start_character=start["character"],
|
||||
end_line=end["line"],
|
||||
end_character=end["character"],
|
||||
severity=diagnostic.get("severity"),
|
||||
code_repr=cls._diagnostic_code_repr(diagnostic.get("code")),
|
||||
source=diagnostic.get("source"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_code_repr(code: Any) -> str | None:
|
||||
if code is None:
|
||||
return None
|
||||
try:
|
||||
return json.dumps(code, sort_keys=True, ensure_ascii=False)
|
||||
except TypeError:
|
||||
return repr(code)
|
||||
|
||||
|
||||
class PublishedDiagnosticsSnapshot:
|
||||
def __init__(self, edited_file_paths: Iterable[EditedFilePath], symbol_retriever: "LanguageServerSymbolRetriever"):
|
||||
generation_by_after_path: dict[str, int] = {}
|
||||
warning_identities_by_before_path: dict[str, set[DiagnosticIdentity]] = {}
|
||||
|
||||
for edited_file_path in edited_file_paths:
|
||||
try:
|
||||
language_server = symbol_retriever.get_language_server(edited_file_path.after_relative_path)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
generation_by_after_path[edited_file_path.after_relative_path] = language_server.get_published_diagnostics_generation(
|
||||
edited_file_path.after_relative_path
|
||||
)
|
||||
|
||||
cached_diagnostics = language_server.get_cached_published_text_document_diagnostics(
|
||||
edited_file_path.before_relative_path,
|
||||
min_severity=2,
|
||||
)
|
||||
if cached_diagnostics is None:
|
||||
try:
|
||||
cached_diagnostics = language_server.request_text_document_diagnostics(
|
||||
edited_file_path.before_relative_path,
|
||||
min_severity=2,
|
||||
)
|
||||
except Exception:
|
||||
cached_diagnostics = []
|
||||
warning_identities_by_before_path[edited_file_path.before_relative_path] = {
|
||||
DiagnosticIdentity.from_diagnostic(diagnostic) for diagnostic in cached_diagnostics or []
|
||||
}
|
||||
|
||||
self.generation_by_after_path = generation_by_after_path
|
||||
self.warning_identities_by_before_path = warning_identities_by_before_path
|
||||
|
||||
|
||||
class GroupedDiagnostics:
|
||||
def __init__(self) -> None:
|
||||
self._grouped_diagnostics: dict[str, dict[str, dict[str, list[dict[str, Any]]]]] = {}
|
||||
|
||||
def add(self, relative_path: str, name_path: str, diagnostic: ls_types.Diagnostic) -> None:
|
||||
severity_name = self._diagnostic_severity_name(diagnostic.get("severity"))
|
||||
self._grouped_diagnostics.setdefault(relative_path, {}).setdefault(severity_name, {}).setdefault(name_path, []).append(
|
||||
self._diagnostic_output_dict(diagnostic)
|
||||
)
|
||||
|
||||
def get_dict(self) -> dict[str, dict[str, dict[str, list[dict[str, Any]]]]]:
|
||||
"""
|
||||
Returns a nested dictionary of the form:
|
||||
{
|
||||
relative_file_path: {
|
||||
severity_name: {
|
||||
name_path: [
|
||||
diagnostic_dict,
|
||||
...
|
||||
],
|
||||
...
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
where:
|
||||
- relative_file_path is the relative path of the file containing the diagnostic
|
||||
- severity_name is the name of the diagnostic severity (e.g. "Warning", "Error")
|
||||
- name_path is the name path of the symbol that owns the diagnostic (or "<file>" if no owner symbol was found)
|
||||
- diagnostic_dict is a dictionary containing the diagnostic's message, range, and optionally code and source
|
||||
"""
|
||||
return self._grouped_diagnostics
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_severity_name(severity: int | None) -> str:
|
||||
if severity is None:
|
||||
return "Unknown"
|
||||
try:
|
||||
return DiagnosticSeverity(severity).name
|
||||
except ValueError:
|
||||
return f"Severity_{severity}"
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_output_dict(diagnostic: ls_types.Diagnostic) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {
|
||||
"message": diagnostic["message"],
|
||||
"range": diagnostic["range"],
|
||||
}
|
||||
if "code" in diagnostic:
|
||||
result["code"] = diagnostic["code"]
|
||||
if "source" in diagnostic:
|
||||
result["source"] = diagnostic["source"]
|
||||
return result
|
||||
|
||||
|
||||
class DiagnosticsDiff:
|
||||
def __init__(
|
||||
self,
|
||||
before_snapshot: PublishedDiagnosticsSnapshot,
|
||||
edited_files: Iterable[EditedFilePath],
|
||||
symbol_retriever: "LanguageServerSymbolRetriever",
|
||||
):
|
||||
grouped_diagnostics = GroupedDiagnostics()
|
||||
|
||||
for edited_file_path in edited_files:
|
||||
try:
|
||||
language_server = symbol_retriever.get_language_server(edited_file_path.after_relative_path)
|
||||
except:
|
||||
continue
|
||||
|
||||
published_diagnostics = language_server.request_published_text_document_diagnostics(
|
||||
relative_file_path=edited_file_path.after_relative_path,
|
||||
after_generation=before_snapshot.generation_by_after_path.get(edited_file_path.after_relative_path, -1),
|
||||
timeout=2.5,
|
||||
min_severity=2,
|
||||
allow_cached=True,
|
||||
)
|
||||
if not published_diagnostics:
|
||||
try:
|
||||
published_diagnostics = language_server.request_text_document_diagnostics(
|
||||
edited_file_path.after_relative_path,
|
||||
min_severity=2,
|
||||
)
|
||||
except:
|
||||
published_diagnostics = None
|
||||
if published_diagnostics is None:
|
||||
continue
|
||||
|
||||
existing_warning_identities = before_snapshot.warning_identities_by_before_path.get(
|
||||
edited_file_path.before_relative_path, set()
|
||||
)
|
||||
new_warning_identities: set[DiagnosticIdentity] = set()
|
||||
|
||||
for diagnostic in published_diagnostics:
|
||||
diagnostic_identity = DiagnosticIdentity.from_diagnostic(diagnostic)
|
||||
if diagnostic_identity in existing_warning_identities or diagnostic_identity in new_warning_identities:
|
||||
continue
|
||||
new_warning_identities.add(diagnostic_identity)
|
||||
|
||||
diagnostic_start = diagnostic["range"]["start"]
|
||||
owner_symbol = symbol_retriever.find_diagnostic_owner_symbol(
|
||||
relative_file_path=edited_file_path.after_relative_path,
|
||||
line=diagnostic_start["line"],
|
||||
column=diagnostic_start["character"],
|
||||
)
|
||||
name_path = owner_symbol.get_name_path() if owner_symbol is not None else "<file>"
|
||||
grouped_diagnostics.add(edited_file_path.after_relative_path, name_path, diagnostic)
|
||||
|
||||
self._grouped_diagnostics = grouped_diagnostics
|
||||
|
||||
def get_grouped_diagnostics(self) -> GroupedDiagnostics:
|
||||
return self._grouped_diagnostics
|
||||
@@ -18,12 +18,64 @@ from solidlsp.ls import (
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ShellCheck binary release info; download directly from upstream GitHub releases rather than
|
||||
# relying on the `shellcheck` npm wrapper (which lazily downloads on first invocation and
|
||||
# turned the bash LS install into a fragile, network-dependent step).
|
||||
_SHELLCHECK_VERSION = "0.10.0"
|
||||
_SHELLCHECK_RELEASE_BASE = f"https://github.com/koalaman/shellcheck/releases/download/v{_SHELLCHECK_VERSION}"
|
||||
_SHELLCHECK_ALLOWED_HOSTS = (
|
||||
"github.com",
|
||||
"objects.githubusercontent.com",
|
||||
"release-assets.githubusercontent.com",
|
||||
)
|
||||
# Per-platform archive metadata: tar.xz on POSIX (extracts to shellcheck-v<ver>/shellcheck),
|
||||
# zip on Windows (extracts to shellcheck.exe at archive root).
|
||||
_SHELLCHECK_DEPENDENCIES: dict[PlatformId, dict[str, str]] = {
|
||||
PlatformId.LINUX_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.linux.x86_64.tar.xz",
|
||||
"sha256": "6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87",
|
||||
},
|
||||
PlatformId.LINUX_arm64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.linux.aarch64.tar.xz",
|
||||
"sha256": "324a7e89de8fa2aed0d0c28f3dab59cf84c6d74264022c00c22af665ed1a09bb",
|
||||
},
|
||||
PlatformId.OSX_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.darwin.x86_64.tar.xz",
|
||||
"sha256": "ef27684f23279d112d8ad84e0823642e43f838993bbb8c0963db9b58a90464c2",
|
||||
},
|
||||
PlatformId.OSX_arm64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.darwin.aarch64.tar.xz",
|
||||
"sha256": "bbd2f14826328eee7679da7221f2bc3afb011f6a928b848c80c321f6046ddf81",
|
||||
},
|
||||
PlatformId.WIN_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.zip",
|
||||
"sha256": "eb6cd53a54ea97a56540e9d296ce7e2fa68715aa507ff23574646c1e12b2e143",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _shellcheck_install_dir(bash_ls_dir: str) -> str:
|
||||
return os.path.join(bash_ls_dir, "shellcheck")
|
||||
|
||||
|
||||
def _shellcheck_binary_path(bash_ls_dir: str) -> str:
|
||||
"""
|
||||
Returns the path to the extracted ShellCheck binary. POSIX archives extract under
|
||||
``shellcheck-v<ver>/shellcheck``; the Windows zip drops ``shellcheck.exe`` at archive root.
|
||||
"""
|
||||
install_dir = _shellcheck_install_dir(bash_ls_dir)
|
||||
if os.name == "nt":
|
||||
return os.path.join(install_dir, "shellcheck.exe")
|
||||
return os.path.join(install_dir, f"shellcheck-v{_SHELLCHECK_VERSION}", "shellcheck")
|
||||
|
||||
|
||||
class BashLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
|
||||
@@ -53,7 +105,7 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Setup runtime dependencies for Bash Language Server and return the command to start the server.
|
||||
"""
|
||||
# Verify both node and npm are installed
|
||||
# verify node + npm are available for the bash-language-server install
|
||||
is_node_installed = shutil.which("node") is not None
|
||||
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
|
||||
is_npm_installed = shutil.which("npm") is not None
|
||||
@@ -61,29 +113,29 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
bash_language_server_version = self._custom_settings.get("bash_language_server_version", "5.6.0")
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="bash-language-server",
|
||||
description="bash-language-server package",
|
||||
command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Install bash-language-server if not already installed
|
||||
bash_ls_dir = os.path.join(self._ls_resources_dir, "bash-lsp")
|
||||
bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server")
|
||||
|
||||
# Handle Windows executable extension
|
||||
managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin")
|
||||
bash_executable_path = os.path.join(managed_bin_dir, "bash-language-server")
|
||||
if os.name == "nt":
|
||||
bash_executable_path += ".cmd"
|
||||
|
||||
# install bash-language-server via npm
|
||||
if not os.path.exists(bash_executable_path):
|
||||
log.info(f"Bash Language Server executable not found at {bash_executable_path}. Installing...")
|
||||
deps.install(bash_ls_dir)
|
||||
log.info("Bash language server dependencies installed successfully")
|
||||
bls_deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="bash-language-server",
|
||||
description="bash-language-server package",
|
||||
command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
log.info("Installing bash-language-server...")
|
||||
bls_deps.install(bash_ls_dir)
|
||||
|
||||
# install ShellCheck binary directly from upstream releases for the current platform
|
||||
self._install_shellcheck_if_missing(bash_ls_dir)
|
||||
|
||||
if not os.path.exists(bash_executable_path):
|
||||
raise FileNotFoundError(
|
||||
@@ -92,6 +144,49 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
|
||||
return bash_executable_path
|
||||
|
||||
@staticmethod
|
||||
def _install_shellcheck_if_missing(bash_ls_dir: str) -> None:
|
||||
"""
|
||||
Downloads and extracts the platform-appropriate ShellCheck release into
|
||||
``${bash_ls_dir}/shellcheck`` if the binary is not already present.
|
||||
"""
|
||||
binary_path = _shellcheck_binary_path(bash_ls_dir)
|
||||
if os.path.exists(binary_path):
|
||||
return
|
||||
|
||||
install_dir = _shellcheck_install_dir(bash_ls_dir)
|
||||
os.makedirs(install_dir, exist_ok=True)
|
||||
|
||||
release = _SHELLCHECK_DEPENDENCIES.get(PlatformUtils.get_platform_id())
|
||||
if release is None:
|
||||
raise RuntimeError(f"ShellCheck has no upstream binary release for platform {PlatformUtils.get_platform_id().value}")
|
||||
|
||||
archive_type = "zip" if os.name == "nt" else "xztar"
|
||||
log.info(f"Downloading ShellCheck v{_SHELLCHECK_VERSION} for {PlatformUtils.get_platform_id().value}")
|
||||
FileUtils.download_and_extract_archive_verified(
|
||||
release["url"],
|
||||
install_dir,
|
||||
archive_type,
|
||||
expected_sha256=release["sha256"],
|
||||
allowed_hosts=_SHELLCHECK_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
if not os.path.exists(binary_path):
|
||||
raise FileNotFoundError(f"ShellCheck binary not found at {binary_path} after extraction; archive layout may have changed.")
|
||||
|
||||
# ensure the binary is executable on POSIX (zip extraction does not preserve perms)
|
||||
if os.name != "nt":
|
||||
current = os.stat(binary_path).st_mode
|
||||
os.chmod(binary_path, current | 0o111)
|
||||
|
||||
def create_launch_command_env(self) -> dict[str, str]:
|
||||
bash_ls_dir = os.path.join(self._ls_resources_dir, "bash-lsp")
|
||||
managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin")
|
||||
return {
|
||||
"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", ""),
|
||||
"SHELLCHECK_PATH": _shellcheck_binary_path(bash_ls_dir),
|
||||
}
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "start"]
|
||||
|
||||
|
||||
@@ -206,6 +206,10 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
scripts. If you have a custom .NET installation, ensure 'dotnet' is available in PATH with version 10 or higher.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
|
||||
|
||||
@@ -152,6 +152,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
```
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a new EclipseJDTLS instance initializing the language server settings appropriately.
|
||||
|
||||
@@ -11,6 +11,7 @@ import threading
|
||||
from overrides import override
|
||||
from sensai.util.logging import LogTime
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
@@ -32,9 +33,10 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
Creates an ElmLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
elm_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
|
||||
managed_bin_dir = os.path.join(self.ls_resources_dir(solidlsp_settings), "elm-lsp", "node_modules", ".bin")
|
||||
|
||||
# Resolve ELM_HOME to absolute path if it's set to a relative path
|
||||
env = {}
|
||||
env = {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")}
|
||||
elm_home = os.environ.get("ELM_HOME")
|
||||
if elm_home:
|
||||
if not os.path.isabs(elm_home):
|
||||
@@ -65,9 +67,15 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Setup runtime dependencies for Elm Language Server and return the command to start the server.
|
||||
"""
|
||||
# Check if elm-language-server is already installed globally
|
||||
elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM)
|
||||
elm_language_server_version = elm_config.get("elm_language_server_version", "2.8.0")
|
||||
elm_compiler_version = elm_config.get("elm_compiler_version", "0.19.1-6")
|
||||
npm_registry = elm_config.get("npm_registry")
|
||||
|
||||
# Check if elm-language-server and elm are already installed globally
|
||||
system_elm_ls = shutil.which("elm-language-server")
|
||||
if system_elm_ls:
|
||||
system_elm = shutil.which("elm")
|
||||
if system_elm_ls and system_elm:
|
||||
log.info(f"Found system-installed elm-language-server at {system_elm_ls}")
|
||||
return [system_elm_ls, "--stdio"]
|
||||
|
||||
@@ -76,10 +84,6 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS 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."
|
||||
elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM)
|
||||
elm_language_server_version = elm_config.get("elm_language_server_version", "2.8.0")
|
||||
npm_registry = elm_config.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
@@ -88,6 +92,12 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
command=build_npm_install_command("@elm-tooling/elm-language-server", elm_language_server_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="elm",
|
||||
description="Elm compiler package",
|
||||
command=build_npm_install_command("elm", elm_compiler_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -98,6 +108,10 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
log.info(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...")
|
||||
with LogTime("Installation of Elm language server dependencies", logger=log):
|
||||
deps.install(elm_ls_dir)
|
||||
elif not system_elm:
|
||||
log.info("Elm compiler not found on PATH. Installing a managed Elm compiler...")
|
||||
with LogTime("Installation of Elm compiler dependency", logger=log):
|
||||
deps.install(elm_ls_dir)
|
||||
|
||||
if not os.path.exists(elm_ls_executable_path):
|
||||
raise FileNotFoundError(
|
||||
@@ -128,6 +142,7 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
@@ -203,3 +218,26 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 1.0
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=-1,
|
||||
timeout=10.0,
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -11,7 +11,7 @@ import shutil
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, LSPConstants, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
@@ -220,6 +220,7 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rangeFormatting": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
@@ -284,3 +285,34 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
|
||||
# Fortran Language Server is ready after initialization
|
||||
log.info("Fortran Language Server initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(uri)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
self.server.notify.did_save_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -23,6 +23,10 @@ class Gopls(SolidLanguageServer):
|
||||
Provides Go specific instantiation of the LanguageServer class using gopls.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
# For Go projects, we should ignore:
|
||||
|
||||
@@ -215,7 +215,14 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True},
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [1, 2]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"synchronization": {"dynamicRegistration": True, "didSave": True},
|
||||
"completion": {"completionItem": {"snippetSupport": True}},
|
||||
"definition": {},
|
||||
"references": {},
|
||||
|
||||
@@ -10,7 +10,7 @@ from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticTag, InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
@@ -115,6 +115,21 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
"""Define language-specific directories to ignore for Julia projects."""
|
||||
return super().is_ignored_dirname(dirname) or dirname in [".julia", "build", "dist"]
|
||||
|
||||
@override
|
||||
def _supports_pull_diagnostics(self) -> bool:
|
||||
# LanguageServer.jl raises an unhandled error on textDocument/diagnostic which crashes
|
||||
# the server process. Force the published-diagnostics path instead.
|
||||
return False
|
||||
|
||||
@override
|
||||
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
|
||||
# LanguageServer.jl needs significant warm-up after init before it emits the first
|
||||
# publishDiagnostics: workspace/configuration round-trip + initial linter pass typically
|
||||
# land 13-15s after the file is opened in a cold LS. Module-scoped fixtures usually have
|
||||
# the cache pre-populated and return immediately, so this ceiling only kicks in for cold
|
||||
# runs (standalone test invocations, CI cold start).
|
||||
return 30.0
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Julia Language Server.
|
||||
@@ -125,8 +140,19 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"capabilities": {
|
||||
"workspace": {"workspaceFolders": True},
|
||||
# workspace.configuration MUST be true: LanguageServer.jl pulls all julia.lint.*
|
||||
# settings via workspace/configuration after initialize. Without it, the server
|
||||
# skips that codepath and runlinter stays disabled, so no diagnostics ever arrive.
|
||||
"workspace": {"workspaceFolders": True, "configuration": True},
|
||||
"textDocument": {
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [DiagnosticTag.Unnecessary, DiagnosticTag.Deprecated]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"synchronization": {"dynamicRegistration": True, "didSave": True},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {"dynamicRegistration": True},
|
||||
@@ -150,6 +176,19 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
def workspace_configuration(params: dict) -> list[Any]:
|
||||
"""
|
||||
Respond to LanguageServer.jl's workspace/configuration pull request.
|
||||
|
||||
The server requests a flat list of julia.* settings (in particular julia.lint.run,
|
||||
position 11 in the response). Returning ``None`` for each entry causes the server to
|
||||
apply its built-in defaults — including ``runlinter = true`` — which is what we want
|
||||
for diagnostics to be produced.
|
||||
"""
|
||||
items = params.get("items", []) if isinstance(params, dict) else []
|
||||
return [None for _ in items]
|
||||
|
||||
self.server.on_request("workspace/configuration", workspace_configuration)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
@@ -166,4 +205,10 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
assert "documentSymbolProvider" in init_response["capabilities"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# nudge the server to pull config: LanguageServer.jl only invokes request_julia_config
|
||||
# from its workspace/didChangeConfiguration handler, so without this notification the
|
||||
# server never asks us for julia.lint.run and runlinter stays disabled (no diagnostics).
|
||||
self.server.notify.workspace_did_change_configuration({"settings": {}})
|
||||
|
||||
log.info("Julia Language Server is initialized and ready.")
|
||||
@@ -124,6 +124,7 @@ class Lean4LanguageServer(SolidLanguageServer):
|
||||
"documentationFormat": ["markdown", "plaintext"],
|
||||
},
|
||||
},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
|
||||
@@ -242,7 +242,7 @@ class OcamlLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
ocaml_ok = self._ocaml_version >= (5, 2, 0)
|
||||
lsp_ok = self._lsp_version >= self.MIN_LSP_VERSION_FOR_CROSS_FILE_REFS
|
||||
return ocaml_ok and lsp_ok
|
||||
return ocaml_ok and lsp_ok and self._index_built
|
||||
|
||||
@staticmethod
|
||||
def _build_ocaml_index_static(repository_root_path: str) -> bool:
|
||||
@@ -340,6 +340,14 @@ class OcamlLanguageServer(SolidLanguageServer):
|
||||
"configuration": True,
|
||||
},
|
||||
"textDocument": {
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [1, 2]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
|
||||
"synchronization": {
|
||||
"dynamicRegistration": True,
|
||||
"willSave": True,
|
||||
|
||||
@@ -777,6 +777,14 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
if value:
|
||||
initialization_options[var] = value
|
||||
|
||||
initialization_options.update(
|
||||
{
|
||||
"checkSyntax": True,
|
||||
"publishDiagnostics": True,
|
||||
"showSyntaxErrors": True,
|
||||
}
|
||||
)
|
||||
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
@@ -831,6 +839,7 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rangeFormatting": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
|
||||
@@ -5,6 +5,8 @@ Contains various configurations and settings specific to PowerShell scripting.
|
||||
You can pass the following entries in ``ls_specific_settings["powershell"]``:
|
||||
- pses_version: Override the pinned PowerShell Editor Services version
|
||||
downloaded by Serena (default: the bundled Serena version).
|
||||
- psscriptanalyzer_version: Override the pinned PSScriptAnalyzer version
|
||||
saved into the bundled PowerShell Editor Services module path.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -12,6 +14,7 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from collections.abc import Hashable
|
||||
@@ -19,7 +22,8 @@ from pathlib import Path
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import LSPConstants, RawDocumentSymbol, SolidLanguageServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.ls_utils import FileUtils
|
||||
@@ -33,6 +37,7 @@ log = logging.getLogger(__name__)
|
||||
PSES_VERSION = "4.4.0"
|
||||
PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc"
|
||||
PSES_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
|
||||
PSSCRIPTANALYZER_VERSION = "1.25.0"
|
||||
|
||||
|
||||
class PowerShellLanguageServer(SolidLanguageServer):
|
||||
@@ -157,6 +162,29 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
|
||||
# The bundled modules path is the directory containing PowerShellEditorServices
|
||||
bundled_modules_path = str(Path(pses_path).parent)
|
||||
psscriptanalyzer_version = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL).get(
|
||||
"psscriptanalyzer_version", PSSCRIPTANALYZER_VERSION
|
||||
)
|
||||
psscriptanalyzer_path = Path(bundled_modules_path) / "PSScriptAnalyzer" / psscriptanalyzer_version
|
||||
if not psscriptanalyzer_path.exists():
|
||||
log.info(f"PSScriptAnalyzer {psscriptanalyzer_version} not found. Installing...")
|
||||
subprocess.run(
|
||||
[
|
||||
pwsh_path,
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
(
|
||||
"Save-Module "
|
||||
"-Name PSScriptAnalyzer "
|
||||
f"-RequiredVersion '{psscriptanalyzer_version}' "
|
||||
f"-Path '{bundled_modules_path}' "
|
||||
"-Force "
|
||||
"-ErrorAction Stop"
|
||||
),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
return pwsh_path, pses_path, bundled_modules_path
|
||||
|
||||
@@ -345,3 +373,35 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
self.server_ready.set()
|
||||
else:
|
||||
log.info("PowerShell Editor Services initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
published_uri = self._get_published_diagnostics_uri(uri)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(published_uri)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
self.server.notify.did_save_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=published_uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -155,6 +155,52 @@ class PyrightServer(SolidLanguageServer):
|
||||
self.found_source_files = True
|
||||
self.analysis_complete.set()
|
||||
|
||||
def handle_pyright_progress_notification(progress_kind: str, params: object | None) -> None:
|
||||
"""Tracks Pyright-specific progress notifications.
|
||||
|
||||
Pyright can emit custom progress notifications instead of only using
|
||||
``$/progress``. Handling them avoids noisy unhandled-method warnings
|
||||
and provides an additional signal that initial analysis has quiesced.
|
||||
"""
|
||||
# normalizing the notification payload
|
||||
message_text = ""
|
||||
percentage: object | None = None
|
||||
if isinstance(params, dict):
|
||||
raw_message = params.get("message")
|
||||
message_text = "" if raw_message is None else str(raw_message)
|
||||
percentage = params.get("percentage")
|
||||
elif params is not None:
|
||||
message_text = str(params)
|
||||
|
||||
progress_label = f"{message_text} ({percentage}%)" if percentage is not None else message_text
|
||||
|
||||
# logging the progress transition
|
||||
if progress_kind == "begin":
|
||||
log.info("Pyright progress started: %s", progress_label)
|
||||
return
|
||||
|
||||
if progress_kind == "report":
|
||||
log.debug("Pyright progress update: %s", progress_label)
|
||||
return
|
||||
|
||||
log.info("Pyright progress finished: %s", progress_label)
|
||||
self.analysis_complete.set()
|
||||
|
||||
def pyright_begin_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/beginProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("begin", params)
|
||||
|
||||
def pyright_report_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/reportProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("report", params)
|
||||
|
||||
def pyright_end_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/endProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("end", params)
|
||||
|
||||
def check_experimental_status(params: dict) -> None:
|
||||
"""
|
||||
Also listen for experimental/serverStatus as a backup signal
|
||||
@@ -170,6 +216,9 @@ class PyrightServer(SolidLanguageServer):
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("pyright/beginProgress", pyright_begin_progress)
|
||||
self.server.on_notification("pyright/reportProgress", pyright_report_progress)
|
||||
self.server.on_notification("pyright/endProgress", pyright_end_progress)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
|
||||
@@ -26,6 +26,10 @@ class RustAnalyzer(SolidLanguageServer):
|
||||
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""Classify rust-analyzer stderr output to avoid false-positive errors."""
|
||||
|
||||
@@ -7,13 +7,17 @@ import glob
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import threading
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, LSPConstants, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
@@ -70,6 +74,7 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
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."
|
||||
solidity_language_server_version = self._custom_settings.get("solidity_language_server_version", "0.8.4")
|
||||
forge_version = self._custom_settings.get("forge_version", "1.5.1")
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
@@ -84,17 +89,29 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
),
|
||||
platform_id="any",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="forge",
|
||||
description="Foundry forge CLI",
|
||||
# the @foundry-rs/forge meta-package's postinstall uses POSIX env-var
|
||||
# syntax that fails on Windows cmd.exe, so we install the platform-specific
|
||||
# subpackage directly to bypass it
|
||||
command=build_npm_install_command(self._get_forge_npm_package(), forge_version, npm_registry),
|
||||
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")
|
||||
managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin")
|
||||
solidity_executable_path = os.path.join(managed_bin_dir, "nomicfoundation-solidity-language-server")
|
||||
forge_executable_path = os.path.join(managed_bin_dir, "forge")
|
||||
|
||||
if os.name == "nt":
|
||||
solidity_executable_path += ".cmd"
|
||||
forge_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...")
|
||||
if not os.path.exists(solidity_executable_path) or not os.path.exists(forge_executable_path):
|
||||
log.info("Solidity Language Server dependencies missing. Installing...")
|
||||
deps.install(solidity_ls_dir)
|
||||
log.info("Solidity language server dependencies installed successfully.")
|
||||
|
||||
@@ -106,6 +123,33 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
|
||||
return solidity_executable_path
|
||||
|
||||
@staticmethod
|
||||
def _get_forge_npm_package() -> str:
|
||||
"""
|
||||
Returns the platform-specific @foundry-rs forge npm subpackage name. Installing
|
||||
the meta-package fails on Windows due to a POSIX-only postinstall script.
|
||||
"""
|
||||
machine = platform.machine().lower()
|
||||
if machine in ("x86_64", "amd64"):
|
||||
arch = "amd64"
|
||||
elif machine in ("arm64", "aarch64"):
|
||||
arch = "arm64"
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported architecture for foundry forge: {machine}")
|
||||
|
||||
if os.name == "nt":
|
||||
if arch != "amd64":
|
||||
raise RuntimeError(f"foundry forge does not publish a Windows {arch} npm package")
|
||||
return "@foundry-rs/forge-win32-amd64"
|
||||
if platform.system() == "Darwin":
|
||||
return f"@foundry-rs/forge-darwin-{arch}"
|
||||
return f"@foundry-rs/forge-linux-{arch}"
|
||||
|
||||
def create_launch_command_env(self) -> dict[str, str]:
|
||||
solidity_ls_dir = os.path.join(self._ls_resources_dir, "solidity-lsp")
|
||||
managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin")
|
||||
return {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")}
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "--stdio"]
|
||||
|
||||
@@ -148,6 +192,7 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
"dynamicRegistration": True,
|
||||
"contentFormat": ["markdown", "plaintext"], # type: ignore[list-item]
|
||||
},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
@@ -224,3 +269,83 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
log.info("No .sol files found; skipping indexing wait")
|
||||
|
||||
log.info("Solidity language server initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
uri = self._get_validation_document_uri(relative_file_path)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(uri)
|
||||
|
||||
absolute_path = pathlib.Path(self.repository_root_path, relative_file_path)
|
||||
contents = absolute_path.read_text(encoding=self._encoding)
|
||||
lines = contents.split("\n")
|
||||
end_position = {
|
||||
"line": len(lines) - 1,
|
||||
"character": len(lines[-1]),
|
||||
}
|
||||
|
||||
self.server.notify.did_open_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
LSPConstants.LANGUAGE_ID: self.language_id,
|
||||
LSPConstants.VERSION: 0,
|
||||
LSPConstants.TEXT: contents,
|
||||
}
|
||||
}
|
||||
)
|
||||
try:
|
||||
self.server.notify.did_change_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
LSPConstants.VERSION: 1,
|
||||
},
|
||||
LSPConstants.CONTENT_CHANGES: [ # type: ignore
|
||||
{
|
||||
LSPConstants.RANGE: {
|
||||
"start": {"line": 0, "character": 0},
|
||||
"end": end_position,
|
||||
},
|
||||
"text": contents,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
finally:
|
||||
self.server.notify.did_close_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
|
||||
def _get_validation_document_uri(self, relative_file_path: str) -> str:
|
||||
absolute_path = pathlib.Path(self.repository_root_path, relative_file_path)
|
||||
|
||||
if os.name != "nt":
|
||||
return absolute_path.as_uri()
|
||||
|
||||
path = absolute_path.as_posix()
|
||||
if len(path) >= 2 and path[1] == ":":
|
||||
path = f"{path[0].lower()}%3A{path[2:]}"
|
||||
|
||||
return f"file:///{path.lstrip('/')}"
|
||||
@@ -67,6 +67,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
- typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3")
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
# Safety timeout for $/progress-based indexing wait. Normally the event fires
|
||||
# well within this window; the timeout is only hit if the server never sends progress.
|
||||
INDEXING_PROGRESS_TIMEOUT = 15.0 if os.name == "nt" else 10.0
|
||||
@@ -85,7 +89,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
self.server_ready = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
|
||||
# Progress tracking for $/progress notifications (project indexing, etc.)
|
||||
# tracking asynchronous diagnostics publication
|
||||
self._published_diagnostics_timeout = 5.0
|
||||
|
||||
# tracking project indexing progress
|
||||
self._progress_lock = threading.Lock()
|
||||
self._active_progress_tokens: set[str] = set()
|
||||
self._indexing_complete = threading.Event()
|
||||
@@ -248,9 +255,11 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
"signatureHelp": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True, "prepareSupport": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"configuration": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"symbol": {"dynamicRegistration": True},
|
||||
},
|
||||
@@ -298,6 +307,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
def execute_client_command_handler(params: dict) -> list:
|
||||
return []
|
||||
|
||||
def configuration_handler(params: dict) -> list:
|
||||
items = params.get("items", [])
|
||||
return [{} for _ in items]
|
||||
|
||||
def do_nothing(params: dict) -> None:
|
||||
return
|
||||
|
||||
@@ -357,6 +370,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
self._indexing_complete.set()
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_request("workspace/configuration", configuration_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
|
||||
@@ -402,6 +416,20 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
self.INDEXING_PROGRESS_TIMEOUT,
|
||||
)
|
||||
|
||||
@override
|
||||
def _get_published_diagnostics_uri(self, request_uri: str) -> str:
|
||||
if os.name != "nt" or not request_uri.startswith("file:///"):
|
||||
return request_uri
|
||||
|
||||
path_part = request_uri[len("file:///") :]
|
||||
if len(path_part) >= 2 and path_part[0].isalpha() and path_part[1] == ":":
|
||||
return f"file:///{path_part[0].lower()}%3A{path_part[2:]}"
|
||||
return request_uri
|
||||
|
||||
@override
|
||||
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
|
||||
return self._published_diagnostics_timeout
|
||||
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 2
|
||||
|
||||
@@ -426,6 +426,18 @@ class VueLanguageServer(SolidLanguageServer):
|
||||
with self._ts_server.open_file(relative_file_path):
|
||||
return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name)
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
self._ensure_ls_operational()
|
||||
assert self._ts_server is not None
|
||||
return self._ts_server.request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity)
|
||||
|
||||
def _forward_edit_to_ts_server_if_needed(self, relative_file_path: str, edit_fn: Callable[[], object]) -> None:
|
||||
"""
|
||||
Calls ``edit_fn`` on the TypeScript server if the file is open there.
|
||||
@@ -598,6 +610,7 @@ class VueLanguageServer(SolidLanguageServer):
|
||||
"signatureHelp": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True, "prepareSupport": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
|
||||
@@ -132,6 +132,7 @@ class ZigLanguageServer(SolidLanguageServer):
|
||||
"dynamicRegistration": True,
|
||||
"contentFormat": ["markdown", "plaintext"],
|
||||
},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
|
||||
+389
-2
@@ -13,8 +13,8 @@ from collections.abc import Hashable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
from pathlib import Path, PurePath
|
||||
from time import perf_counter, sleep
|
||||
from typing import Self, Union, cast
|
||||
from time import monotonic, perf_counter, sleep
|
||||
from typing import Any, Self, Union, cast
|
||||
|
||||
import pathspec
|
||||
from sensai.util.pickle import getstate, load_pickle
|
||||
@@ -515,6 +515,10 @@ class SolidLanguageServer(ABC):
|
||||
self.language_id = language_id
|
||||
self.open_file_buffers: dict[str, LSPFileBuffer] = {}
|
||||
self.language = Language(language_id)
|
||||
self._published_diagnostics: dict[str, list[ls_types.Diagnostic]] = {}
|
||||
self._published_diagnostics_generation_by_uri: dict[str, int] = {}
|
||||
self._published_diagnostics_generation = 0
|
||||
self._published_diagnostics_condition = threading.Condition()
|
||||
|
||||
# initialise symbol caches
|
||||
self.cache_dir = Path(self._solidlsp_settings.project_data_path) / self.CACHE_FOLDER_NAME / self.language_id
|
||||
@@ -554,6 +558,7 @@ class SolidLanguageServer(ABC):
|
||||
logger=logging_fn,
|
||||
start_independent_lsp_process=config.start_independent_lsp_process,
|
||||
)
|
||||
self.server.on_any_notification(self._observe_server_notification)
|
||||
|
||||
# Set up the pathspec matcher for the ignored paths
|
||||
# for all absolute paths in ignored_paths, convert them to relative paths
|
||||
@@ -571,6 +576,388 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
self._has_waited_for_cross_file_references = False
|
||||
|
||||
def _observe_server_notification(self, method: str, params: Any) -> None:
|
||||
"""
|
||||
Observe notifications sent by the language server.
|
||||
|
||||
This is used for generic cross-language bookkeeping that must work independently of
|
||||
language-specific notification handlers.
|
||||
"""
|
||||
if method == "textDocument/publishDiagnostics":
|
||||
self._store_published_diagnostics(params)
|
||||
|
||||
def _store_published_diagnostics(self, params: Any) -> None:
|
||||
"""
|
||||
Store diagnostics received through ``textDocument/publishDiagnostics``.
|
||||
"""
|
||||
if not isinstance(params, dict):
|
||||
return
|
||||
|
||||
uri = params.get("uri")
|
||||
diagnostics = params.get("diagnostics")
|
||||
if not isinstance(uri, str) or not isinstance(diagnostics, list):
|
||||
return
|
||||
|
||||
normalized_diagnostics: list[ls_types.Diagnostic] = []
|
||||
for diagnostic in diagnostics:
|
||||
if not isinstance(diagnostic, dict):
|
||||
continue
|
||||
if "message" not in diagnostic or "range" not in diagnostic:
|
||||
continue
|
||||
|
||||
normalized_diagnostic: ls_types.Diagnostic = {
|
||||
"uri": uri,
|
||||
"message": diagnostic["message"],
|
||||
"range": diagnostic["range"],
|
||||
}
|
||||
severity = diagnostic.get("severity")
|
||||
if isinstance(severity, int):
|
||||
normalized_diagnostic["severity"] = ls_types.DiagnosticSeverity(severity)
|
||||
|
||||
code = diagnostic.get("code")
|
||||
if isinstance(code, int | str):
|
||||
normalized_diagnostic["code"] = code
|
||||
|
||||
if "source" in diagnostic:
|
||||
normalized_diagnostic["source"] = diagnostic["source"]
|
||||
normalized_diagnostics.append(ls_types.Diagnostic(**normalized_diagnostic))
|
||||
|
||||
# canonicalize the key so lookups using URIs produced by pathlib.Path.as_uri() match
|
||||
# what servers publish (e.g. file:///c%3A/... or file:///c:/... vs. file:///C:/...)
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
|
||||
with self._published_diagnostics_condition:
|
||||
self._published_diagnostics_generation += 1
|
||||
self._published_diagnostics[key] = normalized_diagnostics
|
||||
self._published_diagnostics_generation_by_uri[key] = self._published_diagnostics_generation
|
||||
self._published_diagnostics_condition.notify_all()
|
||||
|
||||
@staticmethod
|
||||
def _canonicalize_published_diagnostics_uri(uri: str) -> str:
|
||||
"""
|
||||
Canonicalizes a ``file://`` URI so that diagnostics published by language servers
|
||||
and lookups based on ``pathlib.Path.as_uri()`` agree on the same key.
|
||||
|
||||
On Windows, servers may publish under ``file:///c%3A/...`` or ``file:///c:/...`` while
|
||||
``pathlib.Path.as_uri()`` produces ``file:///C:/...``. The canonical form uses an
|
||||
upper-case drive letter and a plain colon.
|
||||
"""
|
||||
if os.name != "nt" or not uri.startswith("file:///"):
|
||||
return uri
|
||||
|
||||
# extract the segment after "file:///" up to the next slash and look for a drive letter
|
||||
prefix = "file:///"
|
||||
rest = uri[len(prefix) :]
|
||||
slash = rest.find("/")
|
||||
head = rest if slash < 0 else rest[:slash]
|
||||
tail = "" if slash < 0 else rest[slash:]
|
||||
|
||||
if (len(head) >= 2 and head[0].isalpha() and head[1] == ":") or (
|
||||
len(head) >= 4 and head[0].isalpha() and head[1:4].lower() == "%3a"
|
||||
):
|
||||
head = head[0].upper() + ":"
|
||||
else:
|
||||
return uri
|
||||
|
||||
return prefix + head + tail
|
||||
|
||||
def _get_published_diagnostics_generation(self, uri: str) -> int:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
with self._published_diagnostics_condition:
|
||||
return self._published_diagnostics_generation_by_uri.get(key, -1)
|
||||
|
||||
def _wait_for_published_diagnostics(
|
||||
self,
|
||||
uri: str,
|
||||
after_generation: int,
|
||||
timeout: float,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
deadline = perf_counter() + timeout
|
||||
with self._published_diagnostics_condition:
|
||||
while True:
|
||||
current_generation = self._published_diagnostics_generation_by_uri.get(key, -1)
|
||||
if current_generation > after_generation:
|
||||
return list(self._published_diagnostics.get(key, []))
|
||||
|
||||
remaining_timeout = deadline - perf_counter()
|
||||
if remaining_timeout <= 0:
|
||||
return None
|
||||
self._published_diagnostics_condition.wait(timeout=remaining_timeout)
|
||||
|
||||
def _get_cached_published_diagnostics(self, uri: str) -> list[ls_types.Diagnostic] | None:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
with self._published_diagnostics_condition:
|
||||
diagnostics = self._published_diagnostics.get(key)
|
||||
if diagnostics is None:
|
||||
return None
|
||||
return list(diagnostics)
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_matches_range(diagnostic: ls_types.Diagnostic, start_line: int, end_line: int) -> bool:
|
||||
diagnostic_start_line = diagnostic["range"]["start"]["line"]
|
||||
diagnostic_end_line = diagnostic["range"]["end"]["line"]
|
||||
|
||||
# normalize inverted ranges: some servers (e.g. Julia's JuliaSyntax.jl for unterminated
|
||||
# expressions) emit diagnostics whose end position lies before the start position.
|
||||
# treat such ranges as a single span covering both lines.
|
||||
lo = min(diagnostic_start_line, diagnostic_end_line)
|
||||
hi = max(diagnostic_start_line, diagnostic_end_line)
|
||||
|
||||
# when end_line < 0 the caller imposes no upper bound; only enforce the lower bound.
|
||||
if end_line < 0:
|
||||
return hi >= start_line
|
||||
return lo <= end_line and hi >= start_line
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_matches_min_severity(diagnostic: ls_types.Diagnostic, min_severity: int) -> bool:
|
||||
severity = diagnostic.get("severity")
|
||||
if severity is None:
|
||||
return True
|
||||
return int(severity) <= min_severity
|
||||
|
||||
@classmethod
|
||||
def _filter_diagnostics(
|
||||
cls,
|
||||
diagnostics: list[ls_types.Diagnostic],
|
||||
start_line: int,
|
||||
end_line: int,
|
||||
min_severity: int,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
diagnostics = [d for d in diagnostics if cls._diagnostic_matches_range(d, start_line, end_line)]
|
||||
diagnostics = [d for d in diagnostics if cls._diagnostic_matches_min_severity(d, min_severity)]
|
||||
return diagnostics
|
||||
|
||||
def _validate_text_document_diagnostics_request(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int,
|
||||
end_line: int,
|
||||
min_severity: int,
|
||||
) -> str:
|
||||
if not self.server_started:
|
||||
log.error("request_text_document_diagnostics called before Language Server started")
|
||||
raise SolidLSPException("Language Server not started")
|
||||
if start_line < 0:
|
||||
raise ValueError(f"start_line must be non-negative, got {start_line}")
|
||||
if end_line != -1 and end_line < start_line:
|
||||
raise ValueError(f"end_line must be -1 or >= start_line, got {end_line} < {start_line}")
|
||||
if min_severity not in {1, 2, 3, 4}:
|
||||
raise ValueError(f"min_severity must be one of 1, 2, 3, 4, got {min_severity}")
|
||||
return pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
|
||||
|
||||
def get_published_diagnostics_generation(self, relative_file_path: str) -> int:
|
||||
"""
|
||||
Get the generation number for the latest published diagnostics of a file.
|
||||
|
||||
:param relative_file_path: The relative path of the file.
|
||||
:return: the generation number, or ``-1`` if none were published yet.
|
||||
"""
|
||||
uri = pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
|
||||
return self._get_published_diagnostics_generation(uri)
|
||||
|
||||
def get_cached_published_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
"""
|
||||
Get cached diagnostics received through ``textDocument/publishDiagnostics``.
|
||||
|
||||
:param relative_file_path: The relative path of the file to retrieve diagnostics for.
|
||||
:param start_line: the first 0-based line to include in the result.
|
||||
:param end_line: the last 0-based line to include in the result. ``-1`` means no upper bound.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
:return: the cached diagnostics, or ``None`` if no diagnostics were published yet.
|
||||
"""
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
diagnostics = self._get_cached_published_diagnostics(uri)
|
||||
if diagnostics is None:
|
||||
return None
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
|
||||
def request_published_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
after_generation: int = -1,
|
||||
timeout: float = 2.5,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
allow_cached: bool = True,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
"""
|
||||
Wait for diagnostics received through ``textDocument/publishDiagnostics`` and return them.
|
||||
|
||||
:param relative_file_path: The relative path of the file to retrieve diagnostics for.
|
||||
:param after_generation: only return diagnostics published after this generation. ``-1`` accepts the next publication.
|
||||
:param timeout: the maximum time to wait for a newer publication.
|
||||
:param start_line: the first 0-based line to include in the result.
|
||||
:param end_line: the last 0-based line to include in the result. ``-1`` means no upper bound.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
:param allow_cached: whether to fall back to the current cached diagnostics if no newer publication arrives in time.
|
||||
:return: the published diagnostics, or ``None`` if no diagnostics are available.
|
||||
"""
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
published_uri = self._get_published_diagnostics_uri(uri)
|
||||
diagnostics: list[ls_types.Diagnostic] | None = None
|
||||
|
||||
# keeping the document open
|
||||
with self.open_file(relative_file_path):
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=published_uri,
|
||||
after_generation=after_generation,
|
||||
timeout=timeout,
|
||||
allow_cached=allow_cached,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return None
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
|
||||
def _get_published_diagnostics_uri(self, request_uri: str) -> str:
|
||||
"""
|
||||
Gets the URI under which published diagnostics should be looked up.
|
||||
"""
|
||||
return request_uri
|
||||
|
||||
def _supports_pull_diagnostics(self) -> bool:
|
||||
"""
|
||||
Whether the language server handles ``textDocument/diagnostic`` (LSP 3.17 pull
|
||||
diagnostics) gracefully. Subclasses should override and return ``False`` when the
|
||||
underlying server does not implement pull diagnostics, especially when sending
|
||||
the request would terminate the server (e.g. LanguageServer.jl raises an error
|
||||
for unknown methods that crashes the process).
|
||||
"""
|
||||
return True
|
||||
|
||||
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
|
||||
"""
|
||||
Gets the timeout for waiting on published diagnostics after a diagnostics request.
|
||||
"""
|
||||
return 2.5
|
||||
|
||||
def _accept_published_diagnostics(self, diagnostics: list[ls_types.Diagnostic]) -> bool:
|
||||
"""
|
||||
Determines whether a published diagnostics payload should satisfy the current wait.
|
||||
"""
|
||||
return bool(diagnostics)
|
||||
|
||||
def _wait_for_relevant_published_diagnostics(
|
||||
self,
|
||||
uri: str,
|
||||
after_generation: int,
|
||||
timeout: float,
|
||||
allow_cached: bool = True,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
"""
|
||||
Waits for a published diagnostics payload that is relevant for the current request.
|
||||
"""
|
||||
deadline = monotonic() + timeout
|
||||
current_after_generation = after_generation
|
||||
|
||||
while True:
|
||||
remaining_timeout = deadline - monotonic()
|
||||
if remaining_timeout <= 0:
|
||||
break
|
||||
|
||||
diagnostics = self._wait_for_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=current_after_generation,
|
||||
timeout=remaining_timeout,
|
||||
)
|
||||
if diagnostics is None:
|
||||
break
|
||||
if self._accept_published_diagnostics(diagnostics):
|
||||
return diagnostics
|
||||
current_after_generation = self._get_published_diagnostics_generation(uri)
|
||||
|
||||
if allow_cached:
|
||||
diagnostics = self._get_cached_published_diagnostics(uri)
|
||||
if diagnostics is not None and self._accept_published_diagnostics(diagnostics):
|
||||
return diagnostics
|
||||
|
||||
return None
|
||||
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
"""
|
||||
Raise a [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_diagnostic) request to the Language Server
|
||||
to find diagnostics for the given file. Wait for the response and return the result.
|
||||
|
||||
:param relative_file_path: The relative path of the file to retrieve diagnostics for
|
||||
:param start_line: the first 0-based line to include in the result.
|
||||
:param end_line: the last 0-based line to include in the result. `-1` means no upper bound.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
|
||||
:return: A list of diagnostics for the file
|
||||
"""
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
published_uri = self._get_published_diagnostics_uri(uri)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(published_uri)
|
||||
ret: list[ls_types.Diagnostic] | None = None
|
||||
pull_diagnostics_failed = False
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
response: Any = None
|
||||
# only send pull diagnostics when the server actually supports it; some servers
|
||||
# (e.g. Julia's LanguageServer.jl) hard-error and crash the process on unknown methods
|
||||
if self._supports_pull_diagnostics():
|
||||
try:
|
||||
response = self.server.send.text_document_diagnostic(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
except SolidLSPException as ex:
|
||||
log.debug("Falling back to published diagnostics for %s due to pull-diagnostics error: %s", relative_file_path, ex)
|
||||
response = None
|
||||
pull_diagnostics_failed = True
|
||||
|
||||
if response is not None:
|
||||
assert isinstance(response, dict), (
|
||||
f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
|
||||
)
|
||||
ret = []
|
||||
for item in response["items"]: # type: ignore
|
||||
new_item: ls_types.Diagnostic = {
|
||||
"uri": uri,
|
||||
"severity": item["severity"],
|
||||
"message": item["message"],
|
||||
"range": item["range"],
|
||||
"code": item.get("code"), # type: ignore
|
||||
}
|
||||
if "source" in item:
|
||||
new_item["source"] = item["source"]
|
||||
ret.append(ls_types.Diagnostic(**new_item))
|
||||
|
||||
if not ret:
|
||||
published_diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=published_uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(pull_diagnostics_failed),
|
||||
)
|
||||
if published_diagnostics is not None:
|
||||
ret = published_diagnostics
|
||||
|
||||
if ret is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(ret, start_line, end_line, min_severity)
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
"""
|
||||
Creates the dependency provider for this language server.
|
||||
|
||||
@@ -207,6 +207,12 @@ class Language(str, Enum):
|
||||
case _:
|
||||
return 2
|
||||
|
||||
def supports_implementation_request(self) -> bool:
|
||||
"""
|
||||
Return whether the default language server for this language supports ``textDocument/implementation``.
|
||||
"""
|
||||
return self.get_ls_class().supports_implementation_request()
|
||||
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
match self:
|
||||
case self.PYTHON | self.PYTHON_JEDI | self.PYTHON_TY:
|
||||
|
||||
@@ -150,6 +150,7 @@ class LanguageServerProcess:
|
||||
self._pending_requests: dict[Any, Request] = {}
|
||||
self.on_request_handlers: dict[str, Callable[[Any], Any]] = {}
|
||||
self.on_notification_handlers: dict[str, Callable[[Any], None]] = {}
|
||||
self._notification_observers: list[Callable[[str, Any], None]] = []
|
||||
self._trace_log_fn = logger
|
||||
self.tasks: dict[int, Any] = {}
|
||||
self.task_counter = 0
|
||||
@@ -510,6 +511,12 @@ class LanguageServerProcess:
|
||||
"""
|
||||
self.on_notification_handlers[method] = cb
|
||||
|
||||
def on_any_notification(self, cb: Callable[[str, Any], None]) -> None:
|
||||
"""
|
||||
Register an observer that is invoked for every notification received from the server.
|
||||
"""
|
||||
self._notification_observers.append(cb)
|
||||
|
||||
def _response_handler(self, response: StringDict) -> None:
|
||||
"""
|
||||
Handle the response received from the server for a request, using the id to determine the request
|
||||
@@ -561,6 +568,16 @@ class LanguageServerProcess:
|
||||
"""
|
||||
method = response.get("method", "")
|
||||
params = response.get("params")
|
||||
|
||||
for observer in self._notification_observers:
|
||||
try:
|
||||
observer(method, params)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as ex:
|
||||
if not self._is_shutting_down:
|
||||
log.error("Error handling notification observer for method '%s': %s", method, ex, exc_info=ex)
|
||||
|
||||
handler = self.on_notification_handlers.get(method)
|
||||
if not handler:
|
||||
log.warning("Unhandled method '%s'", method)
|
||||
|
||||
@@ -347,7 +347,7 @@ class Diagnostic(TypedDict):
|
||||
""" The severity of the diagnostic. """
|
||||
message: str
|
||||
""" The diagnostic message. """
|
||||
code: str
|
||||
code: NotRequired[str | int]
|
||||
""" The code of the diagnostic. """
|
||||
source: NotRequired[str]
|
||||
""" The source of the diagnostic, e.g. the name of the tool that produced it. """
|
||||
|
||||
@@ -124,6 +124,14 @@ class TextUtils:
|
||||
end_idx = TextUtils.get_index_from_line_col(text, end_line, end_col)
|
||||
return text[start_idx:end_idx]
|
||||
|
||||
@staticmethod
|
||||
def get_text_in_lines_range(text: str, start_line: int, end_line: int) -> str:
|
||||
"""
|
||||
Returns the text encompassed by the given start and end lines (inclusive).
|
||||
"""
|
||||
lines = text.splitlines(keepends=True)
|
||||
return "".join(lines[start_line : end_line + 1])
|
||||
|
||||
|
||||
class PathUtils:
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil as _sh
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
@@ -41,10 +42,13 @@ _LANGUAGE_REPO_ALIASES: dict[Language, Language] = {
|
||||
Language.CPP_CCLS: Language.CPP,
|
||||
Language.PHP_PHPACTOR: Language.PHP,
|
||||
Language.PYTHON_JEDI: Language.PYTHON,
|
||||
Language.PYTHON_TY: Language.PYTHON,
|
||||
Language.RUBY_SOLARGRAPH: Language.RUBY,
|
||||
Language.PYTHON_TY: Language.PYTHON,
|
||||
}
|
||||
|
||||
PYTHON_LANGUAGE_BACKENDS = [Language.PYTHON, Language.PYTHON_TY]
|
||||
|
||||
|
||||
def get_repo_path(language: Language) -> Path:
|
||||
repo_language = _LANGUAGE_REPO_ALIASES.get(language, language)
|
||||
@@ -329,3 +333,60 @@ def language_tests_enabled(language: Language) -> bool:
|
||||
:return: True if tests for the language are enabled, False otherwise
|
||||
"""
|
||||
return language not in _disabled_languages
|
||||
|
||||
|
||||
def language_supports_implementation(language: Language) -> bool:
|
||||
return language.supports_implementation_request()
|
||||
|
||||
|
||||
def languages_supporting_implementation(*languages: Language) -> list[Language]:
|
||||
return [language for language in languages if language_supports_implementation(language)]
|
||||
|
||||
|
||||
_VERIFIED_IMPLEMENTATION_LANGUAGES = {
|
||||
Language.CSHARP,
|
||||
Language.GO,
|
||||
Language.JAVA,
|
||||
Language.RUST,
|
||||
Language.TYPESCRIPT,
|
||||
}
|
||||
|
||||
|
||||
def language_has_verified_implementation_support(language: Language) -> bool:
|
||||
"""
|
||||
True only for languages where the server advertises implementation support and
|
||||
the repo fixtures contain a verified working go-to-implementation scenario.
|
||||
"""
|
||||
return language in _VERIFIED_IMPLEMENTATION_LANGUAGES and language_supports_implementation(language)
|
||||
|
||||
|
||||
def find_identifier_position(file_path: Path, identifier: str) -> tuple[int, int] | None:
|
||||
pattern = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
||||
with file_path.open(encoding="utf-8") as f:
|
||||
for line_idx, line in enumerate(f):
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
return line_idx, match.start()
|
||||
return None
|
||||
|
||||
|
||||
def find_identifier_pos(
|
||||
file_path: Path,
|
||||
identifier: str,
|
||||
occurrence_index: int = 0,
|
||||
column_offset: int = 0,
|
||||
) -> tuple[int, int] | None:
|
||||
if occurrence_index < 0:
|
||||
raise ValueError("occurrence_index must be non-negative")
|
||||
if column_offset < 0:
|
||||
raise ValueError("column_offset must be non-negative")
|
||||
|
||||
pattern = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
||||
current_index = 0
|
||||
with file_path.open(encoding="utf-8") as f:
|
||||
for line_idx, line in enumerate(f):
|
||||
for match in pattern.finditer(line):
|
||||
if current_index == occurrence_index:
|
||||
return line_idx, match.start() + column_offset
|
||||
current_index += 1
|
||||
return None
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
if then
|
||||
echo "$missingGreeting"
|
||||
fi
|
||||
@@ -0,0 +1,9 @@
|
||||
(ns test-app.diagnostics-sample)
|
||||
|
||||
(defn broken-factory []
|
||||
missing-greeting)
|
||||
|
||||
(defn broken-consumer []
|
||||
(let [value (broken-factory)]
|
||||
(println value)
|
||||
missing-consumer-value))
|
||||
@@ -8,5 +8,10 @@
|
||||
"directory": ".",
|
||||
"command": "g++ -std=c++17 -I . -c b.cpp",
|
||||
"file": "b.cpp"
|
||||
},
|
||||
{
|
||||
"directory": ".",
|
||||
"command": "g++ -std=c++17 -I . -c diagnostics_sample.cpp",
|
||||
"file": "diagnostics_sample.cpp"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
int brokenFactory() {
|
||||
return missingGreeting;
|
||||
}
|
||||
|
||||
int brokenConsumer() {
|
||||
int value = brokenFactory();
|
||||
return value + missingConsumerValue;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
def broken_factory : String
|
||||
missingGreeting
|
||||
end
|
||||
|
||||
broken_consumer = missingConsumerValue
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace TestProject
|
||||
{
|
||||
public static class DiagnosticsSample
|
||||
{
|
||||
public static string BrokenFactory()
|
||||
{
|
||||
return missingGreeting;
|
||||
}
|
||||
|
||||
public static void BrokenConsumer()
|
||||
{
|
||||
string value = BrokenFactory();
|
||||
Console.WriteLine(value);
|
||||
Console.WriteLine(missingConsumerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using TestProject.Services;
|
||||
|
||||
namespace TestProject
|
||||
{
|
||||
@@ -7,10 +8,13 @@ namespace TestProject
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
|
||||
|
||||
var calculator = new Calculator();
|
||||
int result = calculator.Add(5, 3);
|
||||
Console.WriteLine($"5 + 3 = {result}");
|
||||
|
||||
IGreeter greeter = new ConsoleGreeter();
|
||||
Console.WriteLine(greeter.FormatGreeting("World"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +44,4 @@ namespace TestProject
|
||||
return (double)a / b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace TestProject.Services
|
||||
{
|
||||
public class ConsoleGreeter : IGreeter
|
||||
{
|
||||
public string FormatGreeting(string name)
|
||||
{
|
||||
return $"Hello, {name}!";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TestProject.Services
|
||||
{
|
||||
public interface IGreeter
|
||||
{
|
||||
string FormatGreeting(string name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
String brokenFactory() {
|
||||
return 'broken'
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
defmodule DiagnosticsSample do
|
||||
def broken_factory do
|
||||
missing_greeting
|
||||
end
|
||||
|
||||
def broken_consumer do
|
||||
value = broken_factory()
|
||||
{value, missing_consumer_value}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module DiagnosticsSample exposing (brokenFactory)
|
||||
|
||||
brokenFactory : String
|
||||
brokenFactory =
|
||||
"unterminated
|
||||
@@ -0,0 +1,5 @@
|
||||
-module(diagnostics_sample).
|
||||
-export([broken_factory/0, broken_consumer/0]).
|
||||
|
||||
broken_factory() -> MissingGreeting.
|
||||
broken_consumer() -> MissingConsumerValue.
|
||||
@@ -0,0 +1,5 @@
|
||||
program diagnostics_sample
|
||||
implicit none
|
||||
integer :: duplicated_value
|
||||
real :: duplicated_value
|
||||
end program diagnostics_sample
|
||||
@@ -0,0 +1,11 @@
|
||||
module DiagnosticsSample
|
||||
|
||||
let brokenConsumerValue = 1
|
||||
|
||||
let brokenFactory () =
|
||||
missingGreeting
|
||||
|
||||
let brokenConsumer () =
|
||||
let value = brokenFactory ()
|
||||
printfn "%A" value
|
||||
missingConsumerValue
|
||||
@@ -0,0 +1,8 @@
|
||||
module Formatter
|
||||
|
||||
type IGreeter =
|
||||
abstract member FormatGreeting: string -> string
|
||||
|
||||
type ConsoleGreeter() =
|
||||
interface IGreeter with
|
||||
member _.FormatGreeting(name: string) = sprintf "Hello, %s!" name
|
||||
@@ -1,7 +1,9 @@
|
||||
module Program
|
||||
|
||||
open Calculator
|
||||
open Formatter
|
||||
open Models
|
||||
open Shapes
|
||||
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
@@ -24,6 +26,12 @@ let main argv =
|
||||
let calc = CalculatorClass()
|
||||
let classResult = calc.Add(20, 5)
|
||||
printfn "Calculator class: 20 + 5 = %d" classResult
|
||||
|
||||
let greeter: IGreeter = ConsoleGreeter() :> IGreeter
|
||||
printfn "%s" (greeter.FormatGreeting("World"))
|
||||
|
||||
let shape: Shape = Circle(2.0) :> Shape
|
||||
printfn "Area: %.2f" (shape.Area())
|
||||
|
||||
// Test person module
|
||||
let person = PersonModule.createPerson "Alice Smith" 25 (Some "alice@example.com")
|
||||
@@ -34,4 +42,4 @@ let main argv =
|
||||
let fact5 = factorial 5
|
||||
printfn "5! = %d" fact5
|
||||
|
||||
0 // return success
|
||||
0 // return success
|
||||
@@ -0,0 +1,10 @@
|
||||
module Shapes
|
||||
|
||||
[<AbstractClass>]
|
||||
type Shape() =
|
||||
abstract member Area: unit -> float
|
||||
|
||||
type Circle(radius: float) =
|
||||
inherit Shape()
|
||||
|
||||
override _.Area() = System.Math.PI * radius * radius
|
||||
@@ -7,8 +7,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Calculator.fs" />
|
||||
<Compile Include="DiagnosticsSample.fs" />
|
||||
<Compile Include="Formatter.fs" />
|
||||
<Compile Include="Shapes.fs" />
|
||||
<Compile Include="Models/Person.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
func brokenFactory() string {
|
||||
return missingGreeting
|
||||
}
|
||||
|
||||
func brokenConsumer() {
|
||||
value := brokenFactory()
|
||||
_ = value
|
||||
_ = missingConsumerValue
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import "fmt"
|
||||
func main() {
|
||||
fmt.Println("Hello, Go!")
|
||||
Helper()
|
||||
var greeter Greeter = ConsoleGreeter{}
|
||||
fmt.Println(greeter.FormatGreeting("Go"))
|
||||
}
|
||||
|
||||
func Helper() {
|
||||
@@ -22,3 +24,13 @@ func (d *DemoStruct) Value() int {
|
||||
func UsingHelper() {
|
||||
Helper()
|
||||
}
|
||||
|
||||
type Greeter interface {
|
||||
FormatGreeting(name string) string
|
||||
}
|
||||
|
||||
type ConsoleGreeter struct{}
|
||||
|
||||
func (ConsoleGreeter) FormatGreeting(name string) string {
|
||||
return "Hello, " + name + "!"
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.example
|
||||
|
||||
class DiagnosticsSample {
|
||||
static String brokenFactory() {
|
||||
missingGreeting
|
||||
}
|
||||
|
||||
static void brokenConsumer() {
|
||||
def value = brokenFactory()
|
||||
println(value)
|
||||
println(missingConsumerValue)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module DiagnosticsSample where
|
||||
|
||||
brokenFactory :: String
|
||||
brokenFactory = missingGreeting
|
||||
|
||||
brokenConsumer :: String
|
||||
brokenConsumer = missingConsumerValue
|
||||
@@ -0,0 +1,10 @@
|
||||
class DiagnosticsSample {
|
||||
static function brokenFactory():String {
|
||||
return missingGreeting;
|
||||
}
|
||||
|
||||
static function brokenConsumer():Void {
|
||||
trace(brokenFactory());
|
||||
trace(missingConsumerValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
float4 main() : SV_Target {
|
||||
return missingGreeting;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package test_repo;
|
||||
|
||||
public class ConsoleGreeter implements Greeter {
|
||||
@Override
|
||||
public String formatGreeting(String name) {
|
||||
return "Hello, " + name + "!";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package test_repo;
|
||||
|
||||
public class DiagnosticsSample {
|
||||
public static String brokenFactory() {
|
||||
return missingGreeting;
|
||||
}
|
||||
|
||||
public static void brokenConsumer() {
|
||||
String value = brokenFactory();
|
||||
System.out.println(value);
|
||||
System.out.println(missingConsumerValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package test_repo;
|
||||
|
||||
public interface Greeter {
|
||||
String formatGreeting(String name);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ public class Main {
|
||||
Model model = new Model("Cascade");
|
||||
System.out.println(model.getName());
|
||||
acceptModel(model);
|
||||
Greeter greeter = new ConsoleGreeter();
|
||||
System.out.println(greeter.formatGreeting("Cascade"));
|
||||
}
|
||||
public static void acceptModel(Model m) {
|
||||
// Do nothing, just for LSP reference
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "diagnostics",
|
||||
"value":
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
function broken_factory(
|
||||
value = 1
|
||||
end
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,11 @@
|
||||
package test_repo
|
||||
|
||||
fun brokenFactory(): String {
|
||||
return missingGreeting
|
||||
}
|
||||
|
||||
fun brokenConsumer() {
|
||||
val value = brokenFactory()
|
||||
println(value)
|
||||
println(missingConsumerValue)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
def brokenFactory : Nat :=
|
||||
@@ -3,6 +3,7 @@
|
||||
-- main.lua: Entry point for the test application
|
||||
|
||||
local calculator = require("src.calculator")
|
||||
local animals = require("src.animals")
|
||||
local utils = require("src.utils")
|
||||
|
||||
local function print_banner()
|
||||
@@ -50,6 +51,12 @@ local function test_utils()
|
||||
logger:warn("This is a warning")
|
||||
end
|
||||
|
||||
local function test_animals()
|
||||
print("\nTesting Animal Module:")
|
||||
local dog = animals.Dog:new("Rex")
|
||||
print(dog:speak())
|
||||
end
|
||||
|
||||
local function interactive_calculator()
|
||||
print("\nInteractive Calculator (type 'quit' to exit):")
|
||||
while true do
|
||||
@@ -99,11 +106,13 @@ local function main(args)
|
||||
if #args == 0 then
|
||||
test_calculator()
|
||||
test_utils()
|
||||
test_animals()
|
||||
elseif args[1] == "interactive" then
|
||||
interactive_calculator()
|
||||
elseif args[1] == "test" then
|
||||
test_calculator()
|
||||
test_utils()
|
||||
test_animals()
|
||||
print("\nAll tests completed!")
|
||||
else
|
||||
print("Usage: lua main.lua [interactive|test]")
|
||||
@@ -111,4 +120,4 @@ local function main(args)
|
||||
end
|
||||
|
||||
-- Run main function
|
||||
main(arg or {})
|
||||
main(arg or {})
|
||||
@@ -0,0 +1,37 @@
|
||||
---@class Animal
|
||||
local Animal = {}
|
||||
Animal.__index = Animal
|
||||
|
||||
---@param name string
|
||||
---@return Animal
|
||||
function Animal:new(name)
|
||||
local self = setmetatable({}, Animal)
|
||||
self.name = name
|
||||
return self
|
||||
end
|
||||
|
||||
---@return string
|
||||
function Animal:speak()
|
||||
return self.name .. " makes a sound"
|
||||
end
|
||||
|
||||
---@class Dog: Animal
|
||||
local Dog = setmetatable({}, { __index = Animal })
|
||||
Dog.__index = Dog
|
||||
|
||||
---@param name string
|
||||
---@return Dog
|
||||
function Dog:new(name)
|
||||
local self = Animal.new(self, name)
|
||||
return setmetatable(self, Dog)
|
||||
end
|
||||
|
||||
---@return string
|
||||
function Dog:speak()
|
||||
return self.name .. " says woof"
|
||||
end
|
||||
|
||||
return {
|
||||
Animal = Animal,
|
||||
Dog = Dog,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
function value = diagnostics_sample()
|
||||
value = missingGreeting;
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
greeting = missingGreeting;
|
||||
consumer = missingConsumerValue;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
let broken_factory (
|
||||
@@ -1,3 +1,4 @@
|
||||
(library
|
||||
(public_name test_repo)
|
||||
(name test_repo))
|
||||
(name test_repo)
|
||||
(modules test_repo))
|
||||
@@ -0,0 +1,4 @@
|
||||
program DiagnosticsSample;
|
||||
begin
|
||||
writeln(
|
||||
end.
|
||||
@@ -0,0 +1,5 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
print $missingGreeting;
|
||||
print $missingConsumerValue;
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
function brokenFactory(): string {
|
||||
return $missingGreeting;
|
||||
}
|
||||
|
||||
function brokenConsumer(): void {
|
||||
$value = brokenFactory();
|
||||
echo $value;
|
||||
echo $missingConsumerValue;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
function Invoke-BrokenFactory {
|
||||
if ($true) {
|
||||
Write-Output "broken"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
from .models import User
|
||||
|
||||
|
||||
def broken_factory() -> User:
|
||||
return missing_user
|
||||
|
||||
|
||||
def broken_consumer() -> None:
|
||||
created_user = broken_factory()
|
||||
print(created_user)
|
||||
print(undefined_name)
|
||||
@@ -7,3 +7,15 @@ class Calculator
|
||||
a - b
|
||||
end
|
||||
end
|
||||
|
||||
class Greeter
|
||||
def format_greeting(name)
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
class ConsoleGreeter < Greeter
|
||||
def format_greeting(name)
|
||||
"Hello, #{name}!"
|
||||
end
|
||||
end
|
||||
@@ -15,6 +15,8 @@ end
|
||||
def helper_function(number = 42)
|
||||
demo = DemoClass.new(number)
|
||||
Calculator.new.add(demo.value, 10)
|
||||
greeter = ConsoleGreeter.new
|
||||
puts greeter.format_greeting("Ruby")
|
||||
demo.print_value
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
pub fn broken_factory() -> String {
|
||||
missing_greeting
|
||||
}
|
||||
|
||||
pub fn broken_consumer() {
|
||||
let value = broken_factory();
|
||||
println!("{value}");
|
||||
println!("{}", missing_consumer_value);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod diagnostics_sample;
|
||||
|
||||
// This function returns the sum of 2 + 2
|
||||
pub fn add() -> i32 {
|
||||
let res = 2 + 2;
|
||||
@@ -7,3 +9,15 @@ pub fn multiply() -> i32 {
|
||||
2 * 3
|
||||
}
|
||||
|
||||
pub trait Greeter {
|
||||
fn format_greeting(&self, name: &str) -> String;
|
||||
}
|
||||
|
||||
pub struct ConsoleGreeter;
|
||||
|
||||
impl Greeter for ConsoleGreeter {
|
||||
fn format_greeting(&self, name: &str) -> String {
|
||||
format!("Hello, {name}!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use rsandbox::add;
|
||||
use rsandbox::{add, ConsoleGreeter, Greeter};
|
||||
|
||||
fn main() {
|
||||
println!("Hello, World!");
|
||||
println!("Good morning!");
|
||||
println!("add result: {}", add());
|
||||
let greeter = ConsoleGreeter;
|
||||
println!("{}", greeter.format_greeting("Rust"));
|
||||
println!("inserted line");
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example
|
||||
|
||||
object DiagnosticsSample {
|
||||
def brokenFactory(): String = missingGreeting
|
||||
def brokenConsumer(): Unit = {
|
||||
println(brokenFactory())
|
||||
println(missingConsumerValue)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract DiagnosticsSample {
|
||||
function brokenFactory() pure returns (string memory) {
|
||||
return "broken";
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ let package = Package(
|
||||
targets: [
|
||||
.target(
|
||||
name: "test_repo",
|
||||
dependencies: []),
|
||||
dependencies: [],
|
||||
path: "src"),
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
func brokenFactory() -> String {
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module diagnostics_sample;
|
||||
initial begin
|
||||
if (
|
||||
end
|
||||
endmodule
|
||||
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user