diff --git a/.github/workflows/black.yaml b/.github/workflows/black.yaml new file mode 100644 index 0000000..1f56b89 --- /dev/null +++ b/.github/workflows/black.yaml @@ -0,0 +1,55 @@ +--- + name: Run black + + defaults: + run: + # To load bashrc + shell: bash -ieo pipefail {0} + + on: + pull_request: + branches: [main, dev] + paths: + - "**/*.py" + schedule: + # run CI every day even if no PRs/merges occur + - cron: '0 12 * * *' + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + jobs: + build: + name: Black + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + mkdir -p .github/linters + cp pyproject.toml .github/linters + + - name: Black + uses: super-linter/super-linter/slim@v4.9.2 + if: always() + env: + # run linter on everything to catch preexisting problems + VALIDATE_ALL_CODEBASE: true + DEFAULT_BRANCH: master + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Run only black + VALIDATE_PYTHON_BLACK: true + PYTHON_BLACK_CONFIG_FILE: pyproject.toml + FILTER_REGEX_EXCLUDE: .*tests/.*.(json|zip|sol) \ No newline at end of file diff --git a/.github/workflows/matchers/pylint.json b/.github/workflows/matchers/pylint.json new file mode 100644 index 0000000..4d9e13f --- /dev/null +++ b/.github/workflows/matchers/pylint.json @@ -0,0 +1,32 @@ +{ + "problemMatcher": [ + { + "owner": "pylint-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + }, + { + "owner": "pylint-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} \ No newline at end of file diff --git a/.github/workflows/pip-audit.yaml b/.github/workflows/pip-audit.yaml new file mode 100644 index 0000000..d3b74ae --- /dev/null +++ b/.github/workflows/pip-audit.yaml @@ -0,0 +1,39 @@ +--- + name: pip-audit + + on: + push: + branches: [ dev, main ] + pull_request: + branches: [ dev, main ] + schedule: [ cron: "0 7 * * 2" ] + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + jobs: + audit: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install Slither + run: | + python -m venv /tmp/pip-audit-env + source /tmp/pip-audit-env/bin/activate + + python -m pip install --upgrade pip setuptools wheel + python -m pip install . + + - name: Run pip-audit + uses: pypa/gh-action-pip-audit@v1.0.8 + with: + virtual-environment: /tmp/pip-audit-env \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..bc82add --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,53 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + build-release: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Build distributions + run: | + python -m pip install --upgrade pip + python -m pip install build + python -m build + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: slither-lsp-dists + path: dist/ + + publish: + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write # For trusted publishing + codesigning. + contents: write # For attaching signing artifacts to the release. + needs: + - build-release + steps: + - name: fetch dists + uses: actions/download-artifact@v4 + with: + name: slither-lsp-dists + path: dist/ + + - name: publish + uses: pypa/gh-action-pypi-publish@v1.8.14 + + - name: sign + uses: sigstore/gh-action-sigstore-python@v2.1.1 + with: + inputs: ./dist/*.tar.gz ./dist/*.whl + release-signing-artifacts: true \ No newline at end of file diff --git a/.github/workflows/pylint.yaml b/.github/workflows/pylint.yaml new file mode 100644 index 0000000..f040b81 --- /dev/null +++ b/.github/workflows/pylint.yaml @@ -0,0 +1,60 @@ +--- + name: Run pylint + + defaults: + run: + # To load bashrc + shell: bash -ieo pipefail {0} + + on: + pull_request: + branches: [main, dev] + paths: + - "**/*.py" + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + jobs: + build: + name: Lint Code Base + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + mkdir -p .github/linters + cp pyproject.toml .github/linters + pip install . + + - name: Register pylint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/pylint.json" + + - name: Pylint + uses: super-linter/super-linter/slim@v6.1.1 + if: always() + env: + # Run linters only on new files for pylint to speed up the CI + VALIDATE_ALL_CODEBASE: false + # Compare against the base branch + # This is only accessible on PR + DEFAULT_BRANCH: ${{ github.base_ref }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Run only pylint + VALIDATE_PYTHON: true + VALIDATE_PYTHON_PYLINT: true + PYTHON_PYLINT_CONFIG_FILE: pyproject.toml + FILTER_REGEX_EXCLUDE: .*tests/.*.(json|zip|sol) \ No newline at end of file diff --git a/README.md b/README.md index 38dadc3..95fa108 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,25 @@ -# Slither Language Server Protocol +# Slither Language Server + +## How to install + +Run the following command from the project root directory (preferably inside a Python virtual environment): + + python -m pip install . ## Features -* TODO \ No newline at end of file + +* Go to implementations/definitions +* Find all references +* Show call hierarchy +* Show type hierarchy +* View and filter detector results + +## Adding new features + +New request handlers should be registered in the [constructor of `SlitherServer`](https://github.com/crytic/slither-lsp/blob/4e951da5244b15b69a5cbf4ce2444f205a0d0417/slither_lsp/app/slither_server.py#L120). Please note that in order to keep the conceptual load to a minimum, handlers should not be declared directly in the `SlitherServer` class itself. Instead, related handlers should be declared in a separate module. See [`goto_def_impl_refs.py`](https://github.com/crytic/slither-lsp/blob/c914576b74f748f69738a0a7a38ee6d53bfd1614/slither_lsp/app/request_handlers/goto_def_impl_refs.py) as an example. + +The Slither Language Server uses [`pygls`](https://pygls.readthedocs.io/en/latest/index.html) as the LSP implementation, and you should refer to its documentation when writing new handlers. + +If you're adding an handler for a standard LSP feature, there will be no need to do anything on the VSCode extension side: VSCode will automatically hook its commands to use the provided feature. + +If, on the other hand, the feature you're trying to add does not map to a standard LSP feature, you will need to register a custom handler. See [`$/slither/analyze`](https://github.com/crytic/slither-lsp/blob/4e951da5244b15b69a5cbf4ce2444f205a0d0417/slither_lsp/app/slither_server.py#L117) as an example: note how each request name is prefixed with `$/slither/`. You will need to manually send request from the VSCode extension in order to trigger these handlers. diff --git a/pyproject.toml b/pyproject.toml index 24be6ed..3af5b2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,38 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "slither-lsp" +description = "Language Server powered by the Slither static analyzer" +version = "0.0.1" +readme = "README.md" +dependencies = [ + "slither-analyzer>=0.10.2", + "semantic-version>=2.10.0", + "pygls>=1.3.0" +] +classifiers = [ + "License :: OSI Approved :: GNU Affero General Public License v3", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Security", +] +requires-python = ">=3.10" + +[[project.authors]] +name = "Trail of Bits" +email = "opensource@trailofbits.com" + +[project.license] +file = "LICENSE" + +[project.urls] +Repository = "https://github.com/crytic/slither-lsp" +Issues = "https://github.com/crytic/slither-lsp/issues" + +[project.scripts] +slither-lsp = "slither_lsp.__main__:main" # Pylint settings @@ -5,8 +40,22 @@ max-line-length = 120 disable = """ -import-outside-toplevel, missing-module-docstring, -useless-return, -duplicate-code +missing-class-docstring, +missing-function-docstring, +unnecessary-lambda, +cyclic-import, +line-too-long, +invalid-name, +fixme, +too-many-return-statements, +too-many-ancestors, +logging-fstring-interpolation, +logging-not-lazy, +duplicate-code, +import-error, +unsubscriptable-object, +unnecessary-lambda-assignment, +too-few-public-methods, +too-many-instance-attributes """ \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 761fdd4..0000000 --- a/setup.py +++ /dev/null @@ -1,24 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="slither-lsp", - description="Language Server powered by the Slither static analyzer", - url="https://github.com/crytic/slither-lsp", - author="Trail of Bits", - version="0.0.1", - packages=find_packages(), - python_requires=">=3.7", - install_requires=[ - "slither-analyzer>=0.8.0", - "pymitter>=0.3.1", - "semantic-version>=2.8.5" - ], - dependency_links=[], - license="AGPL-3.0", - long_description=open("README.md").read(), - entry_points={ - "console_scripts": [ - "slither-lsp = slither_lsp.__main__:main", - ] - }, -) diff --git a/slither_lsp/__main__.py b/slither_lsp/__main__.py index 6213cc0..249728d 100644 --- a/slither_lsp/__main__.py +++ b/slither_lsp/__main__.py @@ -1,14 +1,14 @@ import argparse - import logging -from slither_lsp.app.app import SlitherLSPApp +from slither_lsp.app.app import create_server + -logging.basicConfig() -logging.getLogger("slither_lsp").setLevel(logging.INFO) +def _parse_loglevel(value: str) -> int: + return getattr(logging, value.upper()) -def parse_args() -> argparse.Namespace: +def _parse_args() -> argparse.Namespace: """ Parse the underlying arguments for the program. :return: Returns the arguments for the program. @@ -16,15 +16,41 @@ def parse_args() -> argparse.Namespace: # Initialize our argument parser parser = argparse.ArgumentParser( description="slither-lsp", - usage="slither-lsp [options]", + ) + parser.add_argument("--loglevel", type=_parse_loglevel, default="WARNING") + subcommands = parser.add_subparsers(dest="mode") + tcp = subcommands.add_parser( + "tcp", help="Starts a TCP server instead of communicating over STDIO" + ) + ws = subcommands.add_parser( + "websocket", + help="Starts a WebSocket server instead of communicating over STDIO", + ) + + tcp.add_argument( + "--port", + help="The port the TCP server should be listening.", + type=int, + default=12345, + ) + tcp.add_argument( + "--host", + help="The host the TCP server should be listening on.", + type=str, + default="127.0.0.1", ) - # We want to offer a switch to communicate over a network socket rather than stdin/stdout. - parser.add_argument( + ws.add_argument( "--port", - help="Indicates that the RPC server should use a TCP socket with the provided port, rather " - "than stdio.", - type=int + help="The port the WebSocket server should be listening.", + type=int, + default=12345, + ) + ws.add_argument( + "--host", + help="The host the WebSocket server should be listening on.", + type=str, + default="127.0.0.1", ) # TODO: Perform validation for port number @@ -38,11 +64,19 @@ def main() -> None: :return: None """ # Parse all arguments - args = parse_args() + args = _parse_args() + logger = logging.getLogger("slither_lsp") + logger.setLevel(args.loglevel) + + app = create_server(logger) # Run our main app - app = SlitherLSPApp(port=args.port) - app.start() + if args.mode == "tcp": + app.start_tcp(args.host, args.port) + elif args.mode == "ws": + app.start_ws(args.host, args.port) + else: + app.start_io() if __name__ == "__main__": diff --git a/slither_lsp/app/app.py b/slither_lsp/app/app.py index c171142..57673f8 100644 --- a/slither_lsp/app/app.py +++ b/slither_lsp/app/app.py @@ -1,120 +1,20 @@ -import inspect -from threading import Lock -from typing import Optional, Type, List, Tuple +from importlib.metadata import version as pkg_version +from logging import Logger -from crytic_compile import CryticCompile, InvalidCompilation -from crytic_compile.platform.solc_standard_json import SolcStandardJson -from slither import Slither +import slither_lsp.app.types.params as slsp +from slither_lsp.app import request_handlers +from slither_lsp.app.slither_server import SlitherServer -from slither_lsp.app.app_hooks import SlitherLSPHooks -from slither_lsp.app.solidity_workspace import SolidityWorkspace -from slither_lsp.app.types.analysis_structures import AnalysisResult, CompilationTarget, CompilationTargetType -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.requests.workspace.get_workspace_folders import GetWorkspaceFoldersRequest -from slither_lsp.lsp.requests.window.log_message import LogMessageNotification -from slither_lsp.lsp.servers.base_server import BaseServer -from slither_lsp.lsp.servers.console_server import ConsoleServer -from slither_lsp.lsp.servers.network_server import NetworkServer -from slither_lsp.lsp.state.server_config import ServerConfig -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.basic_structures import MessageType, Diagnostic, Range, Position, DiagnosticSeverity -from slither_lsp.lsp.types.capabilities import ServerCapabilities, WorkspaceServerCapabilities, \ - WorkspaceFoldersServerCapabilities, TextDocumentSyncOptions, TextDocumentSyncKind, SaveOptions, \ - WorkspaceFileOperationsServerCapabilities, FileOperationRegistrationOptions, FileOperationFilter, \ - FileOperationPattern, FileOperationPatternKind, FileOperationPatternOptions -from slither_lsp.lsp.types.params import ShowDocumentParams, LogMessageParams, ShowMessageParams -from slither_lsp.lsp.requests.window.show_message import ShowMessageNotification -from slither_lsp.lsp.requests.window.show_document import ShowDocumentRequest -from slither_lsp.lsp.requests.text_document.publish_diagnostics import PublishDiagnosticsNotification, \ - PublishDiagnosticsParams -from slither_lsp.app.request_handlers import registered_handlers +def create_server(logger: Logger): + version = f"v{pkg_version('slither-lsp')}" + server = SlitherServer(logger.getChild("server"), "slither-lsp", version) -class SlitherLSPApp: - def __init__(self, port: Optional[int]): - self.port: Optional[int] = port - self.server: Optional[BaseServer] = None - self.workspace: Optional[SolidityWorkspace] = None + server.feature(slsp.SLITHER_GET_DETECTOR_LIST)(request_handlers.get_detector_list) + server.feature(slsp.SLITHER_GET_VERSION)(request_handlers.get_version) - @property - def initial_server_capabilities(self) -> ServerCapabilities: - """ - Represents the initial server capabilities to start the server with. This signals to the client which - capabilities they can expect to leverage. - :return: Returns the server capabilities to be used with the server. - """ - # Constructor our overall capabilities object. - return ServerCapabilities( - text_document_sync=TextDocumentSyncOptions( - open_close=True, - change=TextDocumentSyncKind.FULL, - will_save=True, - will_save_wait_until=False, - save=SaveOptions( - include_text=True - ) - ), - hover_provider=True, - declaration_provider=True, - definition_provider=True, - type_definition_provider=True, - implementation_provider=True, - references_provider=True, - workspace=WorkspaceServerCapabilities( - workspace_folders=WorkspaceFoldersServerCapabilities( - supported=True, - change_notifications=True - ) - ) - ) + server.feature(slsp.CRYTIC_COMPILE_GET_COMMAND_LINE_ARGUMENTS)( + request_handlers.get_command_line_args + ) - @staticmethod - def _get_additional_request_handlers() -> List[Type[BaseRequestHandler]]: - # Obtain all additional request handler types imported in our registered list and put them in an array - additional_request_handlers: list = [] - for ch in [getattr(registered_handlers, name) for name in dir(registered_handlers)]: - if inspect.isclass(ch) and ch != BaseRequestHandler and issubclass(ch, BaseRequestHandler): - additional_request_handlers.append(ch) - - return additional_request_handlers - - def start(self): - """ - The main entry point for the application layer of slither-lsp. - :return: None - """ - - # Create our hooks to fulfill LSP requests - server_hooks = SlitherLSPHooks(self) - - # Create our server configuration with our application hooks - server_config = ServerConfig( - initial_server_capabilities=self.initial_server_capabilities, - hooks=server_hooks, - additional_request_handlers=self._get_additional_request_handlers() - ) - - # Determine which server provider to use. - if self.port: - # Initialize a network server (using the provided host/port to communicate over TCP). - self.server = NetworkServer(self.port, server_config=server_config) - else: - # Initialize a console server (uses stdio to communicate) - self.server = ConsoleServer(server_config=server_config) - - # Subscribe to our events - self.server.event_emitter.on('client.initialized', self.on_client_initialized) - - # Create our solidity workspace so it can register relevant events - self.workspace = SolidityWorkspace(self) - - # Begin processing request_handlers - self.server.start() - - def on_client_initialized(self): - # TODO: Remove this event handler entirely, it exists temporarily only for testing. - folders = GetWorkspaceFoldersRequest.send(self.server.context) - LogMessageNotification.send(self.server.context, - LogMessageParams(type=MessageType.WARNING, message="TEST LOGGED MSG!")) - ShowMessageNotification.send(self.server.context, - ShowMessageParams(type=MessageType.ERROR, message="TEST SHOWN MSG!")) + return server diff --git a/slither_lsp/app/app_hooks.py b/slither_lsp/app/app_hooks.py deleted file mode 100644 index dad78c7..0000000 --- a/slither_lsp/app/app_hooks.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import Union, List, Optional, Set - -from crytic_compile.utils.naming import Filename - -from slither_lsp.app.utils.file_paths import uri_to_fs_path, fs_path_to_uri -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.state.server_hooks import ServerHooks -from slither_lsp.lsp.types.basic_structures import Location, LocationLink, Range, Position -from slither_lsp.lsp.types.params import ImplementationParams, TypeDefinitionParams, DefinitionParams, \ - DeclarationParams, HoverParams, Hover -from slither.core.source_mapping.source_mapping import Source - - -class SlitherLSPHooks(ServerHooks): - """ - Defines a set of hooks to fulfill core language feature requests for the Language Server Protocol, leveraging - slither for analysis. - """ - - def __init__(self, app): - # Late import to avoid circular reference issues - from slither_lsp.app.app import SlitherLSPApp - - # Set our parameters. - self.app: SlitherLSPApp = app - - @staticmethod - def _source_to_location(source: Source) -> Optional[Location]: - """ - Converts a slither Source mapping object into a Language Server Protocol Location. - :param source: The slither Source mapping object to convert into a Location. - :return: Returns a Location representing the slither Source mapping object. None if no valid mapping exists. - """ - # If there are no mapped lines, we don't return a location. - if len(source.lines) == 0: - return None - - # Otherwise we can return a location fairly easily. - return Location( - uri=fs_path_to_uri(source.filename.absolute), - range=Range( - start=Position( - line=source.lines[0] - 1, - character=source.starting_column - 1 - ), - end=Position( - line=source.lines[-1] - 1, - character=source.ending_column - 1 - ) - ) - ) - - def hover(self, context: ServerContext, params: HoverParams) -> Optional[Hover]: - # TODO: Return a hover object in response, if applicable. - # Example: Hover(contents="TEST HOVER TEXT", range=None) - return None - - def goto_declaration(self, context: ServerContext, params: DeclarationParams) \ - -> Union[Location, List[Location], List[LocationLink], None]: - # TODO: - return None - - def goto_definition(self, context: ServerContext, params: DefinitionParams) \ - -> Union[Location, List[Location], List[LocationLink], None]: - # Compile a list of definitions - definitions = [] - - # Loop through all compilations - with self.app.workspace.analyses_lock: - for analysis_result in self.app.workspace.analyses: - if analysis_result.analysis is not None: - # TODO: Remove this temporary try/catch once we refactor crytic-compile to now throw errors in - # these functions. - try: - # Obtain our filename for this file - target_filename_str: str = uri_to_fs_path(params.text_document.uri) - target_filename = analysis_result.compilation.filename_lookup(target_filename_str) - - # Obtain the offset for this line + character position - target_offset = analysis_result.compilation.get_global_offset_from_line_and_character( - target_filename, - params.position.line + 1, - params.position.character + 1 - ) - - # Obtain sources - sources: Set[Source] = analysis_result.analysis.offset_to_definitions( - target_filename_str, - target_offset - ) - except Exception: - continue - - # Add all definitions from this source. - for source in sources: - source_location: Optional[Location] = self._source_to_location(source) - if source_location is not None: - definitions.append(source_location) - - return definitions - - def goto_type_definition(self, context: ServerContext, params: TypeDefinitionParams) \ - -> Union[Location, List[Location], List[LocationLink], None]: - # TODO: - return None - - def goto_implementation(self, context: ServerContext, params: ImplementationParams) \ - -> Union[Location, List[Location], List[LocationLink], None]: - # TODO: - return None - - def find_references(self, context: ServerContext, params: ImplementationParams) \ - -> Optional[List[Location]]: - # Compile a list of references - references = [] - - # Loop through all compilations - with self.app.workspace.analyses_lock: - for analysis_result in self.app.workspace.analyses: - if analysis_result.analysis is not None: - # TODO: Remove this temporary try/catch once we refactor crytic-compile to now throw errors in - # these functions. - try: - # Obtain our filename for this file - target_filename_str: str = uri_to_fs_path(params.text_document.uri) - target_filename = analysis_result.compilation.filename_lookup(target_filename_str) - - # Obtain the offset for this line + character position - target_offset = analysis_result.compilation.get_global_offset_from_line_and_character( - target_filename, - params.position.line + 1, - params.position.character + 1 - ) - - # Obtain sources - sources: Set[Source] = analysis_result.analysis.offset_to_references( - target_filename_str, - target_offset - ) - except Exception: - continue - - # Add all references from this source. - for source in sources: - source_location: Optional[Location] = self._source_to_location(source) - if source_location is not None: - references.append(source_location) - - return references diff --git a/slither_lsp/app/feature_analyses/slither_diagnostics.py b/slither_lsp/app/feature_analyses/slither_diagnostics.py index c5fef1b..303cb42 100644 --- a/slither_lsp/app/feature_analyses/slither_diagnostics.py +++ b/slither_lsp/app/feature_analyses/slither_diagnostics.py @@ -1,27 +1,37 @@ -from typing import Dict, List, Set +from __future__ import annotations -from slither_lsp.app.types.analysis_structures import AnalysisResult, SlitherDetectorResult, SlitherDetectorSettings +from typing import TYPE_CHECKING, Dict, List, Set + +import lsprotocol.types as lsp +from slither_lsp.app.types.analysis_structures import ( + AnalysisResult, + SlitherDetectorSettings, +) from slither_lsp.app.utils.file_paths import fs_path_to_uri -from slither_lsp.lsp.requests.text_document.publish_diagnostics import PublishDiagnosticsNotification -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.basic_structures import Diagnostic, Range, Position, DiagnosticSeverity -from slither_lsp.lsp.types.params import PublishDiagnosticsParams + +if TYPE_CHECKING: + from slither_lsp.app.slither_server import SlitherServer class SlitherDiagnostics: """ Tracks and reports diagnostics that were derived from AnalysisResults """ - def __init__(self, context: ServerContext): + + def __init__(self, context: SlitherServer): # Set basic parameters self.context = context # Define a lookup of file uri -> diagnostics. This is necessary so we can track non-existent diagnostics. - self.diagnostics: Dict[str, PublishDiagnosticsParams] = {} + self.diagnostics: Dict[str, lsp.PublishDiagnosticsParams] = {} # TODO: Detector filters - def update(self, analysis_results: List[AnalysisResult], detector_settings: SlitherDetectorSettings) -> None: + def update( + self, + analysis_results: List[AnalysisResult], + detector_settings: SlitherDetectorSettings, + ) -> None: """ Generates and tracks the diagnostics for provided analysis results and detector settings. :param analysis_results: Analysis results containing detector results which diagnostics will be generated from. @@ -29,7 +39,7 @@ def update(self, analysis_results: List[AnalysisResult], detector_settings: Slit :return: None """ # Create a new diagnostics array which our current array will be swapped to later. - new_diagnostics: Dict[str, PublishDiagnosticsParams] = {} + new_diagnostics: Dict[str, lsp.PublishDiagnosticsParams] = {} # Convert our hidden checks to a set hidden_checks = set(detector_settings.hidden_checks) @@ -43,7 +53,10 @@ def update(self, analysis_results: List[AnalysisResult], detector_settings: Slit for detector_result in analysis_result.detector_results: # If we don't have any source mappings, skip this. - if len(detector_result.elements) == 0 or detector_result.elements[0].source_mapping is None: + if ( + len(detector_result.elements) == 0 + or detector_result.elements[0].source_mapping is None + ): continue # If our result is for a check we're skipping, do so. @@ -52,31 +65,39 @@ def update(self, analysis_results: List[AnalysisResult], detector_settings: Slit # Obtain the target filename for this finding (the first element is the most significant) target_result_element = detector_result.elements[0] - target_uri = fs_path_to_uri(target_result_element.source_mapping.filename_absolute) + target_uri = fs_path_to_uri( + target_result_element.source_mapping.filename_absolute + ) # Obtain our params for this file uri, or create them if they haven't been yet. params = new_diagnostics.get(target_uri, None) if params is None: - params = PublishDiagnosticsParams(uri=target_uri, version=None, diagnostics=[]) + params = lsp.PublishDiagnosticsParams( + uri=target_uri, version=None, diagnostics=[] + ) new_diagnostics[target_uri] = params # Add our detector result as a diagnostic. params.diagnostics.append( - Diagnostic( - Range( - start=Position( - line=target_result_element.source_mapping.lines[0] - 1, - character=target_result_element.source_mapping.starting_column - 1 + lsp.Diagnostic( + lsp.Range( + start=lsp.Position( + line=target_result_element.source_mapping.lines[0] + - 1, + character=target_result_element.source_mapping.starting_column + - 1, + ), + end=lsp.Position( + line=target_result_element.source_mapping.lines[-1] + - 1, + character=target_result_element.source_mapping.ending_column + - 1, ), - end=Position( - line=target_result_element.source_mapping.lines[-1] - 1, - character=target_result_element.source_mapping.ending_column - 1 - ) ), message=f"[{detector_result.impact.upper()}] {detector_result.description}", - severity=DiagnosticSeverity.INFORMATION, + severity=lsp.DiagnosticSeverity.Information, code=detector_result.check, - source='slither' + source="slither", ) ) @@ -90,7 +111,9 @@ def update(self, analysis_results: List[AnalysisResult], detector_settings: Slit # Loop for each diagnostic and broadcast all of them. for diagnostic_params in self.diagnostics.values(): - PublishDiagnosticsNotification.send(self.context, diagnostic_params) + self.context.publish_diagnostics( + diagnostic_params.uri, diagnostics=diagnostic_params.diagnostics + ) def _clear_single(self, file_uri: str, clear_from_lookup: bool = False) -> None: """ @@ -101,14 +124,7 @@ def _clear_single(self, file_uri: str, clear_from_lookup: bool = False) -> None: :return: None """ # Send empty diagnostics for this file to the client. - PublishDiagnosticsNotification.send( - self.context, - PublishDiagnosticsParams( - uri=file_uri, - version=None, - diagnostics=[] - ) - ) + self.context.publish_diagnostics(file_uri, diagnostics=[]) # Optionally clear this item from the diagnostic lookup if clear_from_lookup: @@ -120,7 +136,7 @@ def clear(self) -> None: :return: None """ # Loop through all diagnostic files, publish new diagnostics for each file with no items. - for file_uri in self.diagnostics.keys(): + for file_uri, _ in self.diagnostics: self._clear_single(file_uri, False) # Clear the dictionary diff --git a/slither_lsp/app/logging/__init__.py b/slither_lsp/app/logging/__init__.py new file mode 100644 index 0000000..162e8b8 --- /dev/null +++ b/slither_lsp/app/logging/__init__.py @@ -0,0 +1 @@ +from .lsp_handler import LSPHandler diff --git a/slither_lsp/app/logging/lsp_handler.py b/slither_lsp/app/logging/lsp_handler.py new file mode 100644 index 0000000..b1b8441 --- /dev/null +++ b/slither_lsp/app/logging/lsp_handler.py @@ -0,0 +1,27 @@ +from logging import Handler, LogRecord, FATAL, ERROR, WARNING, INFO, DEBUG, NOTSET +from pygls.server import LanguageServer +from lsprotocol.types import MessageType + +_level_to_type = { + FATAL: MessageType.Error, + ERROR: MessageType.Error, + WARNING: MessageType.Warning, + INFO: MessageType.Info, + DEBUG: MessageType.Debug, + NOTSET: MessageType.Debug, +} + + +class LSPHandler(Handler): + """ + Forwards log messages to the LSP client + """ + + def __init__(self, server: LanguageServer): + Handler.__init__(self) + self.server = server + + def emit(self, record: LogRecord): + msg = self.format(record) + msg_type = _level_to_type[record.levelno] + self.server.show_message_log(msg, msg_type=msg_type) diff --git a/slither_lsp/app/request_handlers/__init__.py b/slither_lsp/app/request_handlers/__init__.py index e69de29..c0300f0 100644 --- a/slither_lsp/app/request_handlers/__init__.py +++ b/slither_lsp/app/request_handlers/__init__.py @@ -0,0 +1,20 @@ +from .analysis import * +from .call_hierarchy import ( + register_on_get_incoming_calls, + register_on_get_outgoing_calls, + register_on_prepare_call_hierarchy, +) +from .compilation import * +from .goto_def_impl_refs import ( + register_on_find_references, + register_on_goto_definition, + register_on_goto_implementation, +) +from .type_hierarchy import ( + register_on_get_subtypes, + register_on_get_supertypes, + register_on_prepare_type_hierarchy, +) +from .inlay_hints import register_inlay_hints_handlers +from .symbols import register_symbols_handlers +from .code_lens import register_code_lens_handlers diff --git a/slither_lsp/app/request_handlers/analysis/__init__.py b/slither_lsp/app/request_handlers/analysis/__init__.py index e69de29..206027c 100644 --- a/slither_lsp/app/request_handlers/analysis/__init__.py +++ b/slither_lsp/app/request_handlers/analysis/__init__.py @@ -0,0 +1,2 @@ +from .get_detector_list import get_detector_list +from .get_version import get_version diff --git a/slither_lsp/app/request_handlers/analysis/get_detector_list.py b/slither_lsp/app/request_handlers/analysis/get_detector_list.py index ed8764f..02ee451 100644 --- a/slither_lsp/app/request_handlers/analysis/get_detector_list.py +++ b/slither_lsp/app/request_handlers/analysis/get_detector_list.py @@ -1,22 +1,17 @@ -from typing import Any +# pylint: disable=unused-argument +from pygls.server import LanguageServer from slither.__main__ import get_detectors_and_printers, output_detectors_json -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext - -class GetDetectorListHandler(BaseRequestHandler): +def get_detector_list(ls: LanguageServer, params): """ Handler which invokes slither to obtain a list of all detectors and some properties that describe them. """ - method_name = "$/slither/getDetectorList" - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - # Obtain a list of detectors - detectors, _ = get_detectors_and_printers() + # Obtain a list of detectors + detectors, _ = get_detectors_and_printers() - # Obtain the relevant object to be output as JSON. - detector_types_json = output_detectors_json(detectors) - return detector_types_json + # Obtain the relevant object to be output as JSON. + detector_types_json = output_detectors_json(detectors) + return detector_types_json diff --git a/slither_lsp/app/request_handlers/analysis/get_version.py b/slither_lsp/app/request_handlers/analysis/get_version.py index 28439ea..0766e96 100644 --- a/slither_lsp/app/request_handlers/analysis/get_version.py +++ b/slither_lsp/app/request_handlers/analysis/get_version.py @@ -1,21 +1,17 @@ -from typing import Any +# pylint: disable=unused-argument -from pkg_resources import require +from importlib.metadata import version as pkg_version -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext +from pygls.server import LanguageServer -class GetVersion(BaseRequestHandler): +def get_version(ls: LanguageServer, params): """ Handler which retrieves versions for slither, crytic-compile, and related applications. """ - method_name = "$/slither/getVersion" - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - return { - "slither": require("slither-analyzer")[0].version, - "crytic_compile": require("crytic-compile")[0].version, - "slither_lsp": require("slither-lsp")[0].version - } + return { + "slither": pkg_version("slither-analyzer"), + "crytic_compile": pkg_version("crytic-compile"), + "slither_lsp": pkg_version("slither-lsp"), + } diff --git a/slither_lsp/app/request_handlers/analysis/set_detector_settings.py b/slither_lsp/app/request_handlers/analysis/set_detector_settings.py deleted file mode 100644 index acec180..0000000 --- a/slither_lsp/app/request_handlers/analysis/set_detector_settings.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Any - -from slither_lsp.app.types.analysis_structures import SlitherDetectorSettings -from slither_lsp.app.types.params import SetCompilationTargetsParams -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext - - -class SetDetectorSettingsHandler(BaseRequestHandler): - """ - Handler which sets slither detector settings for the server. This manages how/if detector output is presented. - """ - method_name = "$/slither/setDetectorSettings" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - # Validate the structure of our request - params: SlitherDetectorSettings = SlitherDetectorSettings.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'slither.setDetectorSettings', - params=params - ) - - # This returns nothing on success. - return None diff --git a/slither_lsp/app/request_handlers/call_hierarchy.py b/slither_lsp/app/request_handlers/call_hierarchy.py new file mode 100644 index 0000000..e7fe283 --- /dev/null +++ b/slither_lsp/app/request_handlers/call_hierarchy.py @@ -0,0 +1,193 @@ +from collections import defaultdict +from dataclasses import dataclass +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple + +import lsprotocol.types as lsp +from slither.core.declarations import Function +from slither.slithir.operations import HighLevelCall, InternalCall +from slither.utils.source_mapping import get_definition + +from slither_lsp.app.utils.file_paths import fs_path_to_uri, uri_to_fs_path +from slither_lsp.app.utils.ranges import ( + get_object_name_range, + source_to_range, +) + +from .types import Range, to_lsp_range, to_range + +if TYPE_CHECKING: + from slither_lsp.app.slither_server import SlitherServer + + +@dataclass(frozen=True) +class CallItem: + name: str + range: Range + filename: str + offset: int + + +def register_on_prepare_call_hierarchy(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY) + def on_prepare_call_hierarchy( + ls: "SlitherServer", params: lsp.CallHierarchyPrepareParams + ) -> Optional[List[lsp.CallHierarchyItem]]: + """ + `textDocument/prepareCallHierarchy` doesn't actually produce + the call hierarchy in this case, it only detects what objects + we are trying to produce the call hierarchy for. + The data returned from this method will be sent by the client + back to the "get incoming/outgoing calls" later. + """ + res: Dict[Tuple[str, int], lsp.CallHierarchyItem] = {} + + # Obtain our filename for this file + target_filename_str: str = uri_to_fs_path(params.text_document.uri) + + for analysis, comp in ls.get_analyses_containing(target_filename_str): + # Obtain the offset for this line + character position + target_offset = comp.get_global_offset_from_line( + target_filename_str, params.position.line + 1 + ) + # Obtain objects + objects = analysis.offset_to_objects( + target_filename_str, target_offset + params.position.character + ) + for obj in objects: + source = obj.source_mapping + if not isinstance(obj, Function): + continue + offset = get_definition(obj, comp).start + res[(target_filename_str, offset)] = lsp.CallHierarchyItem( + name=obj.canonical_name, + kind=lsp.SymbolKind.Function, + uri=fs_path_to_uri(source.filename.absolute), + range=source_to_range(source), + selection_range=get_object_name_range(obj, comp), + data={ + "filename": target_filename_str, + "offset": offset, + }, + ) + return list(res.values()) + + +def register_on_get_incoming_calls(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.CALL_HIERARCHY_INCOMING_CALLS) + def on_get_incoming_calls( + ls: "SlitherServer", params: lsp.CallHierarchyIncomingCallsParams + ) -> Optional[List[lsp.CallHierarchyIncomingCall]]: + res: Dict[CallItem, Set[Range]] = defaultdict(set) + + # Obtain our filename for this file + # These will have been populated either by + # the initial "prepare call hierarchy" or by + # other calls to "get incoming calls" + target_filename_str = params.item.data["filename"] + target_offset = params.item.data["offset"] + + referenced_functions = [ + obj + for analysis, comp in ls.get_analyses_containing(target_filename_str) + for obj in analysis.offset_to_objects(target_filename_str, target_offset) + if isinstance(obj, Function) + ] + + calls = [ + (f, op, analysis_result.compilation) + for analysis_result in ls.analyses + if analysis_result.analysis is not None + for comp_unit in analysis_result.analysis.compilation_units + for f in comp_unit.functions + for op in f.all_slithir_operations() + if isinstance(op, (InternalCall, HighLevelCall)) + and isinstance(op.function, Function) + ] + + for func in referenced_functions: + for call_from, call, call_comp in calls: + if call.function is not func: + continue + expr_range = source_to_range(call.expression.source_mapping) + func_range = source_to_range(call_from.source_mapping) + item = CallItem( + name=call_from.canonical_name, + range=to_range(func_range), + filename=call_from.source_mapping.filename.absolute, + offset=get_definition(call_from, call_comp).start, + ) + res[item].add(to_range(expr_range)) + return [ + lsp.CallHierarchyIncomingCall( + from_=lsp.CallHierarchyItem( + name=call_from.name, + kind=lsp.SymbolKind.Function, + uri=fs_path_to_uri(call_from.filename), + range=to_lsp_range(call_from.range), + selection_range=to_lsp_range(call_from.range), + data={ + "filename": call_from.filename, + "offset": call_from.offset, + }, + ), + from_ranges=[to_lsp_range(range) for range in ranges], + ) + for (call_from, ranges) in res.items() + ] + + +def register_on_get_outgoing_calls(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.CALL_HIERARCHY_OUTGOING_CALLS) + def on_get_outgoing_calls( + ls: "SlitherServer", params: lsp.CallHierarchyOutgoingCallsParams + ) -> Optional[List[lsp.CallHierarchyOutgoingCall]]: + res: Dict[CallItem, Set[Range]] = defaultdict(set) + + # Obtain our filename for this file + target_filename_str = params.item.data["filename"] + target_offset = params.item.data["offset"] + + for analysis, comp in ls.get_analyses_containing(target_filename_str): + objects = analysis.offset_to_objects(target_filename_str, target_offset) + for obj in objects: + if not isinstance(obj, Function): + continue + calls = [ + op + for op in obj.all_slithir_operations() + if isinstance(op, (InternalCall, HighLevelCall)) + ] + for call in calls: + if not isinstance(call.function, Function): + continue + call_to = call.function + expr_range = source_to_range(call.expression.source_mapping) + func_range = source_to_range(call_to.source_mapping) + item = CallItem( + name=call_to.canonical_name, + range=to_range(func_range), + filename=call_to.source_mapping.filename.absolute, + offset=get_definition(call_to, comp).start, + ) + res[item].add(to_range(expr_range)) + + return [ + lsp.CallHierarchyOutgoingCall( + to=lsp.CallHierarchyItem( + name=call_to.name, + kind=lsp.SymbolKind.Function, + uri=fs_path_to_uri(call_to.filename), + range=to_lsp_range(call_to.range), + selection_range=to_lsp_range(call_to.range), + data={ + "filename": call_to.filename, + "offset": call_to.offset, + }, + ), + from_ranges=[to_lsp_range(range) for range in ranges], + ) + for (call_to, ranges) in res.items() + ] diff --git a/slither_lsp/app/request_handlers/code_lens.py b/slither_lsp/app/request_handlers/code_lens.py new file mode 100644 index 0000000..b403b35 --- /dev/null +++ b/slither_lsp/app/request_handlers/code_lens.py @@ -0,0 +1,51 @@ +from typing import TYPE_CHECKING, List, Optional + +import lsprotocol.types as lsp + +from slither_lsp.app.utils.file_paths import uri_to_fs_path +from slither_lsp.app.utils.ranges import get_object_name_range + +if TYPE_CHECKING: + from slither_lsp.app.slither_server import SlitherServer + + +def register_code_lens_handlers(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TEXT_DOCUMENT_CODE_LENS) + def code_lens( + ls: "SlitherServer", params: lsp.CodeLensParams + ) -> Optional[List[lsp.CodeLens]]: + target_filename_str: str = uri_to_fs_path(params.text_document.uri) + res: List[lsp.CodeLens] = [] + for analysis, comp in ls.get_analyses_containing(target_filename_str): + filename = comp.filename_lookup(target_filename_str) + functions = [ + func + for contract in analysis.contracts + if contract.source_mapping + and contract.source_mapping.filename == filename + for func in contract.functions_and_modifiers_declared + ] + for func in functions: + txt = f"SlithIR for {func.canonical_name}\n\n" + for node in func.nodes: + if node.expression: + txt += f"Expression: {node.expression}\n" + txt += "IRs:\n" + for ir in node.irs: + txt += f"\t{ir}\n" + elif node.irs: + txt += "IRs:\n" + for ir in node.irs: + txt += f"\t{ir}\n" + res.append( + lsp.CodeLens( + range=get_object_name_range(func, comp), + command=lsp.Command( + "Show SlithIR", + "slither.show_slithir", + [func.canonical_name, txt], + ), + ) + ) + return res diff --git a/slither_lsp/app/request_handlers/compilation/__init__.py b/slither_lsp/app/request_handlers/compilation/__init__.py index e69de29..0ffb63f 100644 --- a/slither_lsp/app/request_handlers/compilation/__init__.py +++ b/slither_lsp/app/request_handlers/compilation/__init__.py @@ -0,0 +1 @@ +from .get_command_line_args import get_command_line_args diff --git a/slither_lsp/app/request_handlers/compilation/autogenerate_standard_json.py b/slither_lsp/app/request_handlers/compilation/autogenerate_standard_json.py deleted file mode 100644 index bb63b9d..0000000 --- a/slither_lsp/app/request_handlers/compilation/autogenerate_standard_json.py +++ /dev/null @@ -1,75 +0,0 @@ -import os -from typing import Any, Iterable, Set - -from crytic_compile.platform.solc_standard_json import SolcStandardJson - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext - - -def get_solidity_files(folders: Iterable[str], recursive=True) -> Set: - """ - Loops through all provided folders and obtains a list of all solidity files existing in them. - This skips 'node_module' folders created by npm/yarn. - :param folders: A list of folders to search for Solidity files within. - :param recursive: Indicates if the search for Solidity files should be recursive. - :return: A list of Solidity file paths which were discovered in the provided folders. - """ - # Create our resulting set - solidity_files = set() - for folder in folders: - for root, dirs, files in os.walk(folder): - # Loop through all files and determine if any have a .sol extension - for file in files: - filename_base, file_extension = os.path.splitext(file) - if file_extension is not None and file_extension.lower() == '.sol': - solidity_files.add(os.path.join(root, file)) - - # If recursive, join our set with any other discovered files in subdirectories. - if recursive: - solidity_files.update( - get_solidity_files([os.path.join(root, d) for d in dirs], recursive) - ) - - # Return all discovered solidity files - return solidity_files - - -class AutogenerateStandardJsonHandler(BaseRequestHandler): - """ - Handler which auto-generates solc standard JSON manifests for Solidity files under a given - directory. - """ - method_name = "$/cryticCompile/solcStandardJson/autogenerate" - - @staticmethod - def process(context: ServerContext, params: Any) -> Any: - - # Obtain our target files and folders. - files = set() - folders = set() - if 'files' in params and isinstance(params['files'], list): - files.update(params['files']) - if 'folders' in params and isinstance(params['folders'], list): - folders.update(params['folders']) - - # Get a list of all solidity files in our folders - files.update(get_solidity_files(folders)) - - # TODO: Parse import strings, create remappings for unresolved imports. - # Regex: import\s+[^"]*"([^"]+)".*; - - # TODO: Parse semvers, find incompatibilities, put them into different compilation buckets - # and potentially return data about satisfactory solc versions, which may enable us to - # use solc-select to compile all. - # Regex: pragma\s+solidity\s+(.*); - - # TODO: For now we return a single json manifest, but we want to split them if we have - # version conflicts. - standard_json = SolcStandardJson() - for file in files: - standard_json.add_source_file(file) - - return [ - standard_json.to_dict() - ] diff --git a/slither_lsp/app/request_handlers/compilation/get_command_line_args.py b/slither_lsp/app/request_handlers/compilation/get_command_line_args.py index ac525ca..981d15a 100644 --- a/slither_lsp/app/request_handlers/compilation/get_command_line_args.py +++ b/slither_lsp/app/request_handlers/compilation/get_command_line_args.py @@ -1,45 +1,40 @@ +# pylint: disable=protected-access, unused-argument + from argparse import ArgumentParser -from typing import Any from crytic_compile.cryticparser.cryticparser import init as crytic_parser_init - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext +from pygls.server import LanguageServer -class GetCommandLineArgsHandler(BaseRequestHandler): +def get_command_line_args(ls: LanguageServer, params): """ Handler which obtains data regarding all command line arguments available in crytic-compile. """ - method_name = "$/cryticCompile/getCommandLineArguments" - - @staticmethod - def process(context: ServerContext, params: Any) -> Any: - # Read our argument parser - parser = ArgumentParser() - crytic_parser_init(parser) - - # Loop through all option groups and underlying options and populate our result. - # (this is a bit hacky as it accesses a private variable, but we don't have that much of an option). - results = [] - for arg_group in parser._action_groups: - # Compile a list of args, skipping the help command. - args_in_group = [] - for arg in arg_group._group_actions: - if '--help' not in arg.option_strings: - args_in_group.append({ + + # Read our argument parser + parser = ArgumentParser() + crytic_parser_init(parser) + + # Loop through all option groups and underlying options and populate our result. + # (this is a bit hacky as it accesses a private variable, but we don't have that much of an option). + results = [] + for arg_group in parser._action_groups: + # Compile a list of args, skipping the help command. + args_in_group = [] + for arg in arg_group._group_actions: + if "--help" not in arg.option_strings: + args_in_group.append( + { "names": arg.option_strings, "help": arg.help, "default": arg.default, - "dest": arg.dest - }) - - # If after filtering we still ahve arguments, add this argument group to our results - if len(args_in_group) > 0: - results.append({ - "title": arg_group.title, - "args": args_in_group - }) - - # Return our argument group -> arguments hierarchy. - return results + "dest": arg.dest, + } + ) + + # If after filtering we still ahve arguments, add this argument group to our results + if len(args_in_group) > 0: + results.append({"title": arg_group.title, "args": args_in_group}) + + # Return our argument group -> arguments hierarchy. + return results diff --git a/slither_lsp/app/request_handlers/compilation/set_compilation_targets.py b/slither_lsp/app/request_handlers/compilation/set_compilation_targets.py deleted file mode 100644 index 021abcd..0000000 --- a/slither_lsp/app/request_handlers/compilation/set_compilation_targets.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Any - -from slither_lsp.app.types.params import SetCompilationTargetsParams -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext - - -class SetCompilationTargetsHandler(BaseRequestHandler): - """ - Handler which sets compilation targets for the language server. If empty, auto-compilation is used instead. - """ - method_name = "$/compilation/setCompilationTargets" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - # Validate the structure of our request - params: SetCompilationTargetsParams = SetCompilationTargetsParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'compilation.setCompilationTargets', - params=params - ) - - # This returns nothing on success. - return None diff --git a/slither_lsp/app/request_handlers/goto_def_impl_refs.py b/slither_lsp/app/request_handlers/goto_def_impl_refs.py new file mode 100644 index 0000000..1a024f4 --- /dev/null +++ b/slither_lsp/app/request_handlers/goto_def_impl_refs.py @@ -0,0 +1,107 @@ +# pylint: disable=broad-exception-caught + +from typing import TYPE_CHECKING, Callable, List, Optional, Set + +import lsprotocol.types as lsp +from slither import Slither +from slither.core.source_mapping.source_mapping import Source + +from slither_lsp.app.utils.file_paths import uri_to_fs_path +from slither_lsp.app.utils.ranges import source_to_location + +if TYPE_CHECKING: + from slither_lsp.app.slither_server import SlitherServer + + +def _inspect_analyses( + ls: "SlitherServer", + target_filename_str: str, + line: int, + col: int, + func: Callable[[Slither, int], Set[Source]], +) -> List[lsp.Location]: + # Compile a list of definitions + results = [] + + # Loop through all compilations + for analysis_result in ls.analyses: + if analysis_result.analysis is not None: + # TODO: Remove this temporary try/catch once we refactor crytic-compile to now throw errors in + # these functions. + try: + # Obtain the offset for this line + character position + target_offset = analysis_result.compilation.get_global_offset_from_line( + target_filename_str, line + ) + # Obtain sources + sources = func(analysis_result.analysis, target_offset + col) + except Exception: + continue + else: + # Add all definitions from this source. + for source in sources: + source_location: Optional[lsp.Location] = source_to_location(source) + if source_location is not None: + results.append(source_location) + + return results + + +def register_on_goto_definition(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TEXT_DOCUMENT_DEFINITION) + def on_goto_definition( + ls: "SlitherServer", params: lsp.DefinitionParams + ) -> List[lsp.Location]: + # Obtain our filename for this file + target_filename_str: str = uri_to_fs_path(params.text_document.uri) + + return _inspect_analyses( + ls, + target_filename_str, + params.position.line + 1, + params.position.character, + lambda analysis, offset: analysis.offset_to_definitions( + target_filename_str, offset + ), + ) + + +def register_on_goto_implementation(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TEXT_DOCUMENT_IMPLEMENTATION) + def on_goto_implementation( + ls: "SlitherServer", params: lsp.ImplementationParams + ) -> List[lsp.Location]: + # Obtain our filename for this file + target_filename_str: str = uri_to_fs_path(params.text_document.uri) + + return _inspect_analyses( + ls, + target_filename_str, + params.position.line + 1, + params.position.character, + lambda analysis, offset: analysis.offset_to_implementations( + target_filename_str, offset + ), + ) + + +def register_on_find_references(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TEXT_DOCUMENT_REFERENCES) + def on_find_references( + ls: "SlitherServer", params: lsp.ImplementationParams + ) -> Optional[List[lsp.Location]]: + # Obtain our filename for this file + target_filename_str: str = uri_to_fs_path(params.text_document.uri) + + return _inspect_analyses( + ls, + target_filename_str, + params.position.line + 1, + params.position.character, + lambda analysis, offset: analysis.offset_to_references( + target_filename_str, offset + ), + ) diff --git a/slither_lsp/app/request_handlers/inlay_hints.py b/slither_lsp/app/request_handlers/inlay_hints.py new file mode 100644 index 0000000..73da426 --- /dev/null +++ b/slither_lsp/app/request_handlers/inlay_hints.py @@ -0,0 +1,48 @@ +from typing import TYPE_CHECKING, List, Optional + +import lsprotocol.types as lsp +from slither.utils.function import get_function_id + +from slither_lsp.app.utils.file_paths import uri_to_fs_path +from slither_lsp.app.utils.ranges import get_object_name_range + +if TYPE_CHECKING: + from slither_lsp.app.slither_server import SlitherServer + + +def register_inlay_hints_handlers(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TEXT_DOCUMENT_INLAY_HINT) + def inlay_hints( + ls: "SlitherServer", params: lsp.InlayHintParams + ) -> Optional[List[lsp.InlayHint]]: + """ + Shows the ID of a function next to its definition + """ + # Obtain our filename for this file + target_filename_str: str = uri_to_fs_path(params.text_document.uri) + res: List[lsp.InlayHint] = [] + for analysis, comp in ls.get_analyses_containing(target_filename_str): + filename = comp.filename_lookup(target_filename_str) + + functions = [ + func + for contract in analysis.contracts + if contract.source_mapping + and contract.source_mapping.filename == filename + for func in contract.functions_and_modifiers_declared + if func.visibility in {"public", "external"} + ] + + for func in functions: + function_id = get_function_id(func.solidity_signature) + name_range = get_object_name_range(func, comp) + res.append( + lsp.InlayHint( + position=lsp.Position( + name_range.end.line, name_range.end.character + ), + label=f": {function_id:#0{10}x}", + ) + ) + return res diff --git a/slither_lsp/app/request_handlers/registered_handlers.py b/slither_lsp/app/request_handlers/registered_handlers.py deleted file mode 100644 index 48235ee..0000000 --- a/slither_lsp/app/request_handlers/registered_handlers.py +++ /dev/null @@ -1,11 +0,0 @@ -# pylint: disable=unused-import,relative-beyond-top-level - -# compilation -from slither_lsp.app.request_handlers.compilation.autogenerate_standard_json import AutogenerateStandardJsonHandler -from slither_lsp.app.request_handlers.compilation.get_command_line_args import GetCommandLineArgsHandler -from slither_lsp.app.request_handlers.compilation.set_compilation_targets import SetCompilationTargetsHandler - -# slither -from slither_lsp.app.request_handlers.analysis.get_version import GetVersion -from slither_lsp.app.request_handlers.analysis.get_detector_list import GetDetectorListHandler -from slither_lsp.app.request_handlers.analysis.set_detector_settings import SetDetectorSettingsHandler diff --git a/slither_lsp/app/request_handlers/symbols.py b/slither_lsp/app/request_handlers/symbols.py new file mode 100644 index 0000000..0a9bd2d --- /dev/null +++ b/slither_lsp/app/request_handlers/symbols.py @@ -0,0 +1,82 @@ +# pylint: disable=too-many-branches + +from typing import TYPE_CHECKING, List, Optional + +import lsprotocol.types as lsp + +from slither_lsp.app.utils.file_paths import uri_to_fs_path +from slither_lsp.app.utils.ranges import get_object_name_range, source_to_range + +if TYPE_CHECKING: + from slither_lsp.app.slither_server import SlitherServer + + +def register_symbols_handlers(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) + def document_symbol( + ls: "SlitherServer", params: lsp.DocumentSymbolParams + ) -> Optional[List[lsp.DocumentSymbol]]: + """ + Allows navigation using VSCode "breadcrumbs" + """ + # Obtain our filename for this file + target_filename_str: str = uri_to_fs_path(params.text_document.uri) + res: List[lsp.DocumentSymbol] = [] + + def add_child(children, obj, kind): + children.append( + lsp.DocumentSymbol( + name=obj.name, + kind=kind, + range=source_to_range(obj.source_mapping), + selection_range=get_object_name_range(obj, comp), + ) + ) + + for analysis, comp in ls.get_analyses_containing(target_filename_str): + filename = comp.filename_lookup(target_filename_str) + + for contract in analysis.contracts: + if ( + not contract.source_mapping + or contract.source_mapping.filename != filename + ): + continue + if contract.is_interface: + kind = lsp.SymbolKind.Interface + else: + kind = lsp.SymbolKind.Class + children: List[lsp.DocumentSymbol] = [] + + for struct in contract.structures_declared: + if struct.source_mapping is None: + continue + add_child(children, struct, lsp.SymbolKind.Struct) + + for enum in contract.enums_declared: + if enum.source_mapping is None: + continue + add_child(children, enum, lsp.SymbolKind.Enum) + + for event in contract.events_declared: + if event.source_mapping is None: + continue + add_child(children, event, lsp.SymbolKind.Enum) + + for func in contract.functions_and_modifiers_declared: + if func.source_mapping is None: + continue + add_child(children, func, lsp.SymbolKind.Function) + + res.append( + lsp.DocumentSymbol( + name=contract.name, + kind=kind, + range=source_to_range(contract.source_mapping), + selection_range=get_object_name_range(contract, comp), + children=children, + ) + ) + + return res diff --git a/slither_lsp/app/request_handlers/type_hierarchy.py b/slither_lsp/app/request_handlers/type_hierarchy.py new file mode 100644 index 0000000..22b140d --- /dev/null +++ b/slither_lsp/app/request_handlers/type_hierarchy.py @@ -0,0 +1,198 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, List, Optional, Set + +import lsprotocol.types as lsp +from slither.core.declarations import Contract +from slither.utils.source_mapping import get_definition + +from slither_lsp.app.utils.file_paths import fs_path_to_uri, uri_to_fs_path +from slither_lsp.app.utils.ranges import get_object_name_range + +from .types import Range, to_lsp_range, to_range + +if TYPE_CHECKING: + from slither_lsp.app.slither_server import SlitherServer + + +@dataclass(frozen=True) +class TypeItem: + name: str + range: Range + kind: lsp.SymbolKind + filename: str + offset: int + + +def register_on_prepare_type_hierarchy(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY) + def on_prepare_type_hierarchy( + ls: "SlitherServer", params: lsp.TypeHierarchyPrepareParams + ) -> Optional[List[lsp.TypeHierarchyItem]]: + res: Set[TypeItem] = set() + + # Obtain our filename for this file + target_filename_str: str = uri_to_fs_path(params.text_document.uri) + + for analysis, comp in ls.get_analyses_containing(target_filename_str): + # Obtain the offset for this line + character position + target_offset = comp.get_global_offset_from_line( + target_filename_str, params.position.line + 1 + ) + # Obtain objects + objects = analysis.offset_to_objects( + target_filename_str, target_offset + params.position.character + ) + for obj in objects: + source = obj.source_mapping + if not isinstance(obj, Contract): + continue + offset = get_definition(obj, comp).start + range_ = get_object_name_range(obj, comp) + if obj.is_interface: + kind = lsp.SymbolKind.Interface + else: + kind = lsp.SymbolKind.Class + res.add( + TypeItem( + name=obj.name, + range=to_range(range_), + kind=kind, + filename=source.filename.absolute, + offset=offset, + ) + ) + return [ + lsp.TypeHierarchyItem( + name=item.name, + kind=item.kind, + uri=fs_path_to_uri(item.filename), + range=to_lsp_range(item.range), + selection_range=to_lsp_range(item.range), + data={ + "filename": item.filename, + "offset": item.offset, + }, + ) + for item in res + ] + + +def register_on_get_subtypes(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TYPE_HIERARCHY_SUBTYPES) + def on_get_subtypes( + ls: "SlitherServer", params: lsp.TypeHierarchySubtypesParams + ) -> Optional[List[lsp.TypeHierarchyItem]]: + res: Set[TypeItem] = set() + + # Obtain our filename for this file + # These will have been populated either by + # the initial "prepare call hierarchy" or by + # other calls to "get incoming calls" + target_filename_str = params.item.data["filename"] + target_offset = params.item.data["offset"] + + referenced_contracts = [ + contract + for analysis, _ in ls.get_analyses_containing(target_filename_str) + for contract in analysis.offset_to_objects( + target_filename_str, target_offset + ) + if isinstance(contract, Contract) + ] + + contracts = [ + (contract, analysis_result.compilation) + for analysis_result in ls.analyses + if analysis_result.analysis is not None + for comp_unit in analysis_result.analysis.compilation_units + for contract in comp_unit.contracts + ] + + for contract in referenced_contracts: + for other_contract, other_contract_comp in contracts: + if contract not in other_contract.immediate_inheritance: + continue + range_ = get_object_name_range(other_contract, other_contract_comp) + if other_contract.is_interface: + kind = lsp.SymbolKind.Interface + else: + kind = lsp.SymbolKind.Class + item = TypeItem( + name=other_contract.name, + range=to_range(range_), + kind=kind, + filename=other_contract.source_mapping.filename.absolute, + offset=get_definition(other_contract, other_contract_comp).start, + ) + res.add(item) + return [ + lsp.TypeHierarchyItem( + name=item.name, + kind=item.kind, + uri=fs_path_to_uri(item.filename), + range=to_lsp_range(item.range), + selection_range=to_lsp_range(item.range), + data={ + "filename": item.filename, + "offset": item.offset, + }, + ) + for item in res + ] + + +def register_on_get_supertypes(ls: "SlitherServer"): + @ls.thread() + @ls.feature(lsp.TYPE_HIERARCHY_SUPERTYPES) + def on_get_supertypes( + ls: "SlitherServer", params: lsp.TypeHierarchySupertypesParams + ) -> Optional[List[lsp.TypeHierarchyItem]]: + res: Set[TypeItem] = set() + + # Obtain our filename for this file + # These will have been populated either by + # the initial "prepare call hierarchy" or by + # other calls to "get incoming calls" + target_filename_str = params.item.data["filename"] + target_offset = params.item.data["offset"] + + supertypes = [ + (supertype, comp) + for analysis, comp in ls.get_analyses_containing(target_filename_str) + for contract in analysis.offset_to_objects( + target_filename_str, target_offset + ) + if isinstance(contract, Contract) + for supertype in contract.immediate_inheritance + ] + + for sup, comp in supertypes: + range_ = get_object_name_range(sup, comp) + if sup.is_interface: + kind = lsp.SymbolKind.Interface + else: + kind = lsp.SymbolKind.Class + item = TypeItem( + name=sup.name, + range=to_range(range_), + kind=kind, + filename=sup.source_mapping.filename.absolute, + offset=get_definition(sup, comp).start, + ) + res.add(item) + return [ + lsp.TypeHierarchyItem( + name=item.name, + kind=item.kind, + uri=fs_path_to_uri(item.filename), + range=to_lsp_range(item.range), + selection_range=to_lsp_range(item.range), + data={ + "filename": item.filename, + "offset": item.offset, + }, + ) + for item in res + ] diff --git a/slither_lsp/app/request_handlers/types.py b/slither_lsp/app/request_handlers/types.py new file mode 100644 index 0000000..1eb281a --- /dev/null +++ b/slither_lsp/app/request_handlers/types.py @@ -0,0 +1,22 @@ +from typing import TypeAlias, Tuple +import lsprotocol.types as lsp + +# Type definitions for call hierarchy +Pos: TypeAlias = Tuple[int, int] +Range: TypeAlias = Tuple[Pos, Pos] + + +def to_lsp_pos(pos: Pos) -> lsp.Position: + return lsp.Position(line=pos[0], character=pos[1]) + + +def to_lsp_range(range_: Range) -> lsp.Range: + return lsp.Range(start=to_lsp_pos(range_[0]), end=to_lsp_pos(range_[1])) + + +def to_pos(pos: lsp.Position) -> Pos: + return (pos.line, pos.character) + + +def to_range(range_: lsp.Range) -> Range: + return (to_pos(range_.start), to_pos(range_.end)) diff --git a/slither_lsp/app/requests/analysis/report_analysis_progress.py b/slither_lsp/app/requests/analysis/report_analysis_progress.py deleted file mode 100644 index e30a99d..0000000 --- a/slither_lsp/app/requests/analysis/report_analysis_progress.py +++ /dev/null @@ -1,26 +0,0 @@ -from slither_lsp.app.types.params import AnalysisProgressParams -from slither_lsp.lsp.requests.base_request import BaseRequest -from slither_lsp.lsp.state.server_context import ServerContext - - -class ReportAnalysisProgressNotification(BaseRequest): - """ - Notification which sends analysis results to a client. - """ - - method_name = "$/analysis/reportAnalysisProgress" - - @classmethod - def send(cls, context: ServerContext, params: AnalysisProgressParams) -> None: - """ - Sends a notification to the client to report progress on analysis parameters. - :param context: The server context which determines the server to use to send the message. - :param params: The parameters needed to send the request. - :return: None - """ - - # Invoke the operation otherwise. - context.server.send_notification_message(cls.method_name, params.to_dict()) - - # This is a notification so we return nothing. - return None diff --git a/slither_lsp/app/slither_server.py b/slither_lsp/app/slither_server.py new file mode 100644 index 0000000..708a7eb --- /dev/null +++ b/slither_lsp/app/slither_server.py @@ -0,0 +1,295 @@ +# pylint: disable=broad-exception-caught, protected-access, unused-argument + +import logging +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor +from functools import lru_cache +from threading import Lock +from typing import Dict, List, Optional, Tuple, Type +from os.path import split + +import lsprotocol.types as lsp +from crytic_compile.crytic_compile import CryticCompile +from pygls.lsp import METHOD_TO_OPTIONS +from pygls.protocol import LanguageServerProtocol +from pygls.server import LanguageServer +from slither import Slither +from slither.__main__ import ( + _process as process_detectors_and_printers, +) +from slither.__main__ import ( + get_detectors_and_printers, +) + +from slither_lsp.app.feature_analyses.slither_diagnostics import SlitherDiagnostics +from slither_lsp.app.logging import LSPHandler +from slither_lsp.app.request_handlers import ( + register_on_find_references, + register_on_get_incoming_calls, + register_on_get_outgoing_calls, + register_on_get_subtypes, + register_on_get_supertypes, + register_on_goto_definition, + register_on_goto_implementation, + register_on_prepare_call_hierarchy, + register_on_prepare_type_hierarchy, + register_inlay_hints_handlers, + register_symbols_handlers, + register_code_lens_handlers, +) +from slither_lsp.app.types.analysis_structures import ( + AnalysisResult, + SlitherDetectorResult, + SlitherDetectorSettings, +) +from slither_lsp.app.types.params import ( + METHOD_TO_TYPES, + SLITHER_SET_DETECTOR_SETTINGS, + SLITHER_ANALYZE, + AnalysisRequestParams, +) +from slither_lsp.app.utils.file_paths import normalize_uri, uri_to_fs_path + +# TODO(frabert): Maybe this should be upstreamed? https://github.com/openlawlibrary/pygls/discussions/338 +METHOD_TO_OPTIONS[ + lsp.WORKSPACE_DID_CHANGE_WATCHED_FILES +] = lsp.DidChangeWatchedFilesRegistrationOptions + + +class SlitherProtocol(LanguageServerProtocol): + # See https://github.com/openlawlibrary/pygls/discussions/441 + + @lru_cache + def get_message_type(self, method: str) -> Optional[Type]: + return METHOD_TO_TYPES.get(method, (None,))[0] or super().get_message_type( + method + ) + + @lru_cache + def get_result_type(self, method: str) -> Optional[Type]: + return METHOD_TO_TYPES.get(method, (None, None))[1] or super().get_result_type( + method + ) + + +class SlitherServer(LanguageServer): + _logger: logging.Logger + _init_params: Optional[lsp.InitializeParams] = None + + # Define our workspace parameters. + workspaces: Dict[str, AnalysisResult] = {} + # `workspace_in_progress[uri]` is locked if there's a compilation in progress for the workspace `uri` + workspace_in_progress: Dict[str, Lock] = defaultdict(Lock) + + @property + def analyses(self) -> List[AnalysisResult]: + return list(self.workspaces.values()) + + # Define our slither diagnostics provider + detector_settings: SlitherDetectorSettings = SlitherDetectorSettings( + enabled=True, hidden_checks=[] + ) + + analysis_pool = ThreadPoolExecutor() + + def __init__(self, logger: logging.Logger, *args): + super().__init__(protocol_cls=SlitherProtocol, *args) + + self._logger = logger + self._logger.addHandler(LSPHandler(self)) + self.slither_diagnostics = SlitherDiagnostics(self) + + @self.feature(lsp.INITIALIZE) + def on_initialize(ls: SlitherServer, params): + ls._on_initialize(params) + + @self.feature(lsp.INITIALIZED) + def on_initialized(ls: SlitherServer, params): + ls.show_message("slither-lsp initialized", lsp.MessageType.Debug) + + @self.thread() + @self.feature(lsp.WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS) + def on_did_change_workspace_folder(ls: SlitherServer, params): + ls._on_did_change_workspace_folders(params) + + @self.thread() + @self.feature(SLITHER_SET_DETECTOR_SETTINGS) + def on_set_detector_settings(ls: SlitherServer, params): + ls._on_set_detector_settings(params) + + @self.thread() + @self.feature(SLITHER_ANALYZE) + def on_analyze(ls: SlitherServer, params): + ls._on_analyze(params) + + register_on_goto_definition(self) + register_on_goto_implementation(self) + register_on_find_references(self) + + register_on_prepare_call_hierarchy(self) + register_on_get_incoming_calls(self) + register_on_get_outgoing_calls(self) + + register_on_prepare_type_hierarchy(self) + register_on_get_subtypes(self) + register_on_get_supertypes(self) + + register_inlay_hints_handlers(self) + + register_symbols_handlers(self) + + register_code_lens_handlers(self) + + @property + def workspace_opened(self): + """ + If True, indicates a workspace folder has been opened. + If False, no workspace folder is opened and files in opened tabs will be targeted. + :return: None + """ + return len(self.workspaces.items()) > 0 + + def _on_initialize(self, params: lsp.InitializeParams) -> None: + """ + Sets initial data when the server is spun up, such as workspace folders. + :param params: The client's initialization parameters. + :param result: The server response to the client's initialization parameters. + :return: None + """ + # Set our workspace folder on initialization. + self._init_params = params + for workspace in params.workspace_folders or []: + self.queue_compile_workspace(normalize_uri(workspace.uri)) + + def _on_analyze(self, params: AnalysisRequestParams): + uris = [normalize_uri(uri) for uri in params.uris or self.workspaces.keys()] + for uri in uris: + path = uri_to_fs_path(uri) + workspace_name = split(path)[1] + if self.workspace_in_progress[uri].locked(): + self.show_message( + f"Analysis for {workspace_name} is already in progress", + lsp.MessageType.Warning, + ) + continue + self.queue_compile_workspace(uri) + + def queue_compile_workspace(self, uri: str): + """ + Queues a workspace for compilation. `uri` should be normalized + """ + path = uri_to_fs_path(uri) + workspace_name = split(path)[1] + + def do_compile(): + detector_classes, _ = get_detectors_and_printers() + with self.workspace_in_progress[uri]: + self.show_message( + f"Compilation for {workspace_name} has started", + lsp.MessageType.Info, + ) + try: + compilation = CryticCompile(path) + analysis = Slither(compilation) + _, detector_results, _, _ = process_detectors_and_printers( + analysis, detector_classes, [] + ) + # Parse detector results + if detector_results is not None and isinstance( + detector_results, list + ): + detector_results = [ + SlitherDetectorResult.from_dict(detector_result) + for detector_result in detector_results + ] + else: + detector_results = None + analyzed_successfully = True + analysis_error = None + self.show_message( + f"Compilation for {workspace_name} has completed successfully", + lsp.MessageType.Info, + ) + except Exception as err: + # If we encounter an error, set our status. + analysis = None + compilation = None + analyzed_successfully = False + analysis_error = err + detector_results = None + self.show_message( + f"Compilation for {workspace_name} has failed. See log for details.", + lsp.MessageType.Info, + ) + self._logger.log( + logging.ERROR, "Compiling %s has failed: %s", path, err + ) + + self.workspaces[uri] = AnalysisResult( + succeeded=analyzed_successfully, + compilation=compilation, + analysis=analysis, + error=analysis_error, + detector_results=detector_results, + ) + self._refresh_detector_output() + + self.analysis_pool.submit(do_compile) + + def _on_did_change_workspace_folders( + self, params: lsp.DidChangeWorkspaceFoldersParams + ) -> None: + """ + Applies client-reported changes to the workspace folders. + :param params: The client's workspace change message parameters. + :return: None + """ + for added in params.event.added: + uri = normalize_uri(added.uri) + if not self.workspace_in_progress[uri].locked(): + self.queue_compile_workspace(uri) + for removed in params.event.removed: + uri = normalize_uri(removed.uri) + with self.workspace_in_progress[uri]: + self.workspaces.pop(uri, None) + + def _on_set_detector_settings(self, params: SlitherDetectorSettings) -> None: + """ + Sets the detector settings for the workspace, indicating how detector output should be presented. + :param params: The parameters provided for the set detector settings request. + :return: None + """ + # If our detector settings are not different than existing ones, we do not need to trigger any on-change events. + if params == self.detector_settings: + return + + # Set our detector settings + self.detector_settings = params + + # Refresh our detector output + self._refresh_detector_output() + + def _refresh_detector_output(self): + """ + Refreshes language server state given new analyses output or detector settings. + :return: None + """ + # Update our diagnostics with new detector output. + self.slither_diagnostics.update(self.analyses, self.detector_settings) + + def get_analyses_containing( + self, filename: str + ) -> List[Tuple[Slither, CryticCompile]]: + def lookup(comp: CryticCompile): + try: + return comp.filename_lookup(filename) + except ValueError: + return None + + return [ + (analysis_result.analysis, analysis_result.compilation) + for analysis_result in self.analyses + if analysis_result.analysis is not None + and analysis_result.compilation is not None + and lookup(analysis_result.compilation) is not None + ] diff --git a/slither_lsp/app/solidity_workspace.py b/slither_lsp/app/solidity_workspace.py deleted file mode 100644 index ba3ae1c..0000000 --- a/slither_lsp/app/solidity_workspace.py +++ /dev/null @@ -1,568 +0,0 @@ -import os -import time -import uuid -from dataclasses import dataclass -from threading import Lock -from time import sleep -from typing import Iterable, Set, List, Dict, Optional, Any -from urllib.parse import urlparse, unquote_plus, urljoin -from urllib.request import url2pathname, pathname2url - -from crytic_compile import CryticCompile -from crytic_compile.platform.solc_standard_json import SolcStandardJson -from slither import Slither -from slither.__main__ import get_detectors_and_printers, _process as process_detectors_and_printers -from slither_lsp.app.feature_analyses.slither_diagnostics import SlitherDiagnostics -from slither_lsp.app.requests.analysis.report_analysis_progress import ReportAnalysisProgressNotification - -from slither_lsp.app.types.params import SetCompilationTargetsParams, AnalysisProgressParams, AnalysisResultProgress -from slither_lsp.app.types.analysis_structures import CompilationTarget, CompilationTargetStandardJson, \ - CompilationTargetType, AnalysisResult, SlitherDetectorResult, SlitherDetectorSettings -from slither_lsp.app.utils.file_paths import is_solidity_file, get_solidity_files, fs_path_to_uri, uri_to_fs_path, \ - normalize_uri -from slither_lsp.lsp.request_handlers.workspace.did_change_watched_files import DidChangeWatchedFilesHandler -from slither_lsp.lsp.requests.client.register_capability import RegisterCapabilityRequest -from slither_lsp.lsp.types.basic_structures import WorkspaceFolder, TextDocumentItem -from slither_lsp.lsp.types.params import InitializeResult, InitializeParams, DidChangeWorkspaceFoldersParams, \ - DidOpenTextDocumentParams, DidCloseTextDocumentParams, RegistrationParams, DidChangeWatchedFilesParams, \ - DidChangeTextDocumentParams, \ - FileChangeType -from slither_lsp.lsp.types.registration_options import DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher - - -@dataclass -class SolidityFile: - """ - Describes a Solidity file, it's dependencies, and any version pragma it has. - """ - path: str - - dependencies: List[str] - - version_spec: Any - - -class SolidityWorkspace: - """ - Provides a set of methods for tracking and managing Solidity files in a workspace. - """ - _FILE_CHANGE_POLLING_INTERVAL_SECONDS = 0.2 - _FILE_CHANGE_ANALYSIS_DELAY_SECONDS = 2.0 - - def __init__(self, app): - # Late import to avoid circular reference issues - from slither_lsp.app.app import SlitherLSPApp - - # Set our main parameters. - self.app: SlitherLSPApp = app - self._init_params: Optional[InitializeParams] = None - self._shutdown: bool = False - - # Define our workspace parameters. - self._watch_files_registration_id = str(uuid.uuid4()) # obtain a random uuid for our registration id. - self.workspace_folders: Dict[str, WorkspaceFolder] = {} - self.open_text_documents: Dict[str, TextDocumentItem] = {} - - # Define our analysis variables - - # Define a set of all solidity files available in the workspace which we track with appropriate file events. - self.solidity_file_uris: Set[str] = set() - self.solidity_files_lock = Lock() # lock for updating the above - self._solidity_files_scan_required = True # determines if we need to scan filesystem for all solidity files - - # Define a variable to indicate when a change was made to the workspace that requires reanalysis. - # If None, no changes were made and reanalysis is not necessary. - self._analysis_last_change_time: Optional[float] = 0 - - # Define a set of compilation targets which are autogenerated if not user supplied. - self.compilation_targets: List[CompilationTarget] = [] - self.compilation_targets_lock = Lock() # lock for updating the above - self.compilation_targets_autogenerate = True - self.compilation_targets_enabled = False - - # Define our compilation variables - self.analyses: List[AnalysisResult] = [] - self.analyses_lock = Lock() - - # Define our slither diagnostics provider - self.detector_settings: SlitherDetectorSettings = SlitherDetectorSettings(enabled=True, hidden_checks=[]) - self.slither_diagnostics: Optional[SlitherDiagnostics] = None - - # Register our event handlers. Some are registered synchronously so as not to waste resources spinning up - # a thread. This is fine so long as we do not hang up the thread for long. Any potentially longer running - # event handlers should be run asynchronously. - self.app.server.event_emitter.on( - 'server.initialized', - self._on_server_initialized, - asynchronous=False - ) - self.app.server.event_emitter.on( - 'client.initialized', - self.main_loop, - asynchronous=True - ) - self.app.server.event_emitter.on( - 'workspace.didChangeWorkspaceFolders', - self._on_did_change_workspace_folders, - asynchronous=False - ) - self.app.server.event_emitter.on( - 'workspace.didChangeWatchedFiles', - self._on_did_change_watched_files, - asynchronous=False - ) - self.app.server.event_emitter.on( - 'textDocument.didOpen', - self._on_did_open_text_document, - asynchronous=False - ) - self.app.server.event_emitter.on( - 'textDocument.didClose', - self._on_did_close_text_document, - asynchronous=False - ) - self.app.server.event_emitter.on( - 'textDocument.didChange', - self._on_did_close_text_document, - asynchronous=False - ) - - # Add event handlers for application layer specific commands. - self.app.server.event_emitter.on( - 'compilation.setCompilationTargets', - self._on_set_compilation_targets, - asynchronous=True - ) - self.app.server.event_emitter.on( - 'slither.setDetectorSettings', - self._on_set_detector_settings, - asynchronous=True - ) - - @property - def workspace_opened(self): - """ - If True, indicates a workspace folder has been opened. - If False, no workspace folder is opened and files in opened tabs will be targeted. - :return: None - """ - return len(self.workspace_folders) > 0 - - def main_loop(self) -> None: - """ - Runs the main loop, updating state of the Solidity workspace continuously.. This stops executing when - shutdown() is called, or when the language server receives a shutdown request. - :return: None - """ - # Create an object to track slither detector results in diagnostics - self.slither_diagnostics = SlitherDiagnostics(self.app.server.context) - - # Register for our file watching operations on Solidity files. - RegisterCapabilityRequest.send( - context=self.app.server.context, - params=RegistrationParams( - registrations=[ - DidChangeWatchedFilesHandler.get_registration( - context=self.app.server.context, - registration_id=self._watch_files_registration_id, - registration_options=DidChangeWatchedFilesRegistrationOptions([ - FileSystemWatcher(glob_pattern='**/*.sol', kind=None) - ]) - ) - ] - ) - ) - - # Queue for analysis on startup - self._queue_reanalysis(files_rescan=True, force_analysis=True) - - # Loop while a shutdown was not signalled, we want to keep performing reanalysis if there are valid changes - # requiring it. Similarly, we want to be sure compilation targets are enabled. - while not self._shutdown and not self.app.server.context.shutdown: - # Perform new analysis and change our status. - self.refresh_workspace() - - # Sleep for our polling interval before trying again. - sleep(self._FILE_CHANGE_POLLING_INTERVAL_SECONDS) - - def shutdown(self): - """ - Signals that this workspace object should shutdown. - :return: - """ - self._shutdown = True - - def _queue_reanalysis(self, files_rescan: bool = False, force_analysis: bool = False) -> None: - """ - Queues all compilation targets for reanalysis. - :param files_rescan: If True, indicates the list of Solidity files in the workspace will be - re-evaluated. - :param force_analysis: If True, indicates the last analysis time will be set to zero so a reanalysis - will be triggered in the next polling interval. Otherwise it will be recompiled at the first polling round after - the delay time has elapsed. - :return: None - """ - # Update the tracked solidity files in the workspace if requested. - with self.solidity_files_lock: - if files_rescan: - self._solidity_files_scan_required = True - - # Set our analysis time. - with self.analyses_lock: - self._analysis_last_change_time = 0 if force_analysis else time.time() - - def _on_server_initialized(self, params: InitializeParams, result: InitializeResult) -> None: - """ - Sets initial data when the server is spun up, such as workspace folders. - :param params: The client's initialization parameters. - :param result: The server response to the client's initialization parameters. - :return: None - """ - # Set our workspace folder on initialization. - self._init_params = params - self.workspace_folders = { - workspace_folder.uri: workspace_folder - for workspace_folder in self._init_params.workspace_folders or [] - } - self.open_text_documents = {} - - def _on_did_change_workspace_folders(self, params: DidChangeWorkspaceFoldersParams) -> None: - """ - Applies client-reported changes to the workspace folders. - :param params: The client's workspace change message parameters. - :return: None - """ - # Apply changes to workspace folders. - with self.solidity_files_lock: - for added in params.event.added: - self.workspace_folders[normalize_uri(added.uri)] = added - for removed in params.event.removed: - self.workspace_folders.pop(normalize_uri(removed.uri), None) - - # Trigger a re-scan of the workspace Solidity files and re-analyze the codebase. - self._queue_reanalysis(files_rescan=True, force_analysis=True) - - def _on_did_open_text_document(self, params: DidOpenTextDocumentParams) -> None: - """ - Applies changes to the workspace state as a new file was opened. - :param params: The client's text document opened message parameters. - :return: None - """ - # Update our open text document lookup. - self.open_text_documents[normalize_uri(params.text_document.uri)] = params.text_document - - # If we have no workspace folders open, update our solidity files list and re-analyze immediately. - if not self.workspace_opened: - self._queue_reanalysis(files_rescan=True, force_analysis=True) - - def _on_did_close_text_document(self, params: DidCloseTextDocumentParams) -> None: - """ - Applies changes to the workspace state as an opened file was closed. - :param params: The client's text document closed message parameters. - :return: None - """ - # Update our open text document lookup. - self.open_text_documents.pop(normalize_uri(params.text_document.uri), None) - - # If we have no workspace folders open, update our solidity files list and re-analyze immediately. - if not self.workspace_opened: - self._queue_reanalysis(files_rescan=True, force_analysis=True) - - def _on_did_change_text_document(self, params: DidChangeTextDocumentParams) -> None: - """ - Applies changes to the workspace state as an opened file was changed. - :param params: The client's text document closed message parameters. - :return: None - """ - # TODO: This should invalidate some analysis in this file until it is saved and re-analysis occurs - # via on_did_change_watched_files. - pass - - def _on_did_change_watched_files(self, params: DidChangeWatchedFilesParams) -> None: - """ - Applies changes to the workspace state as files were changed. - :param params: The client's watched file change parameters. - :return: None - """ - # Update our solidity file list - updated_solidity_files = False - with self.solidity_files_lock: - for file_event in params.changes: - target_uri = normalize_uri(file_event.uri) - if file_event.type == FileChangeType.CREATED or file_event.type == FileChangeType.CHANGED: - self.solidity_file_uris.add(target_uri) - elif file_event.type == FileChangeType.DELETED: - self.solidity_file_uris.remove(target_uri) - updated_solidity_files = updated_solidity_files or is_solidity_file(target_uri) - - # Set our analysis pending status to signal for reanalysis. - if updated_solidity_files: - self._queue_reanalysis() - - def _on_set_detector_settings(self, params: SlitherDetectorSettings) -> None: - """ - Sets the detector settings for the workspace, indicating how detector output should be presented. - :param params: The parameters provided for the set detector settings request. - :return: None - """ - # If our detector settings are not different than existing ones, we do not need to trigger any on-change events. - if params == self.detector_settings: - return - - # Set our detector settings - self.detector_settings = params - - # Refresh our detector output - self._refresh_detector_output() - - def _on_set_compilation_targets(self, params: SetCompilationTargetsParams) -> None: - """ - Sets the compilation targets for the workspace to use. If empty, auto-compilation will be used instead. - :param params: The parameters provided for the set compilation request. - :return: None - """ - self.set_compilation_targets(params.targets) - - def set_compilation_targets(self, targets: List[CompilationTarget]) -> None: - """ - Sets the compilation targets to be autogenerated and sets them into a pending generation state. - :return: None - """ - # Update our solidity file list - with self.compilation_targets_lock: - # Update our list of compilation targets - self.compilation_targets = targets - self.compilation_targets_enabled = True - self.compilation_targets_autogenerate = len(self.compilation_targets) == 0 - - # Set our analysis pending status to signal for reanalysis. - self._queue_reanalysis(files_rescan=True, force_analysis=True) - - def _report_compilation_progress(self) -> None: - """ - Reports current analysis progress to the client. - :return: None - """ - # Create a list of progress reports for each analysis result. - report_progress_params_results: List[AnalysisResultProgress] = [] - - # Report on progress for each compilation target - for compilation_target in self.compilation_targets: - # See if we can find a corresponding analysis. - corresponding_analysis: Optional[AnalysisResult] = None - for analysis in self.analyses: - if analysis.compilation_target == compilation_target: - corresponding_analysis = analysis - - # Add progress for this compilation target - report_progress_params_results.append( - AnalysisResultProgress( - succeeded=None if corresponding_analysis is None else corresponding_analysis.succeeded, - compilation_target=compilation_target, - error=None if corresponding_analysis is None or corresponding_analysis.error is None - else str(corresponding_analysis.error) - ) - ) - - # Send the reports to the client. - report_progress_params = AnalysisProgressParams(results=report_progress_params_results) - ReportAnalysisProgressNotification.send( - context=self.app.server.context, - params=report_progress_params - ) - - def _refresh_detector_output(self): - """ - Refreshes language server state given new analyses output or detector settings. - :return: None - """ - # Update our diagnostics with new detector output. - self.slither_diagnostics.update(self.analyses, self.detector_settings) - - def refresh_workspace(self) -> None: - """ - Refreshes the currently opened workspace state, tracking new files if a workspace change occurred, re-running - compilation and analysis if needed, and refreshing analysis output such as for slither detectors. - :return: - """ - # First refresh our initial solidity target list for this workspace - with self.solidity_files_lock: - # If we're meant to re-scan our solidity files, do so to get an initial collection of solidity target - # locations. This can happen on initialization or workspace folder change. - if self._solidity_files_scan_required: - # If we have no workspace folders, we'll instead use our open text documents as targets. - if not self.workspace_opened: - self.solidity_file_uris = set([ - open_doc.uri - for open_doc in self.open_text_documents.values() - if is_solidity_file(open_doc.uri) - ]) - else: - solidity_file_paths = get_solidity_files([ - uri_to_fs_path(workspace_folder.uri) - for workspace_folder in self.workspace_folders.values() - ]) - self.solidity_file_uris = set([fs_path_to_uri(fspath) for fspath in solidity_file_paths]) - self._solidity_files_scan_required = False - - # Regenerate new compilation targets if desired. - with self.compilation_targets_lock: - if self.compilation_targets_autogenerate: - self.compilation_targets = self.generate_compilation_targets() - - # Acquire locks for compilation + analysis, and see if our last change time exceeds our delay or we are forcing - # recompilation. - with self.compilation_targets_lock: - with self.analyses_lock: - if self._analysis_last_change_time is not None and self.compilation_targets_enabled: - if time.time() - self._analysis_last_change_time > self._FILE_CHANGE_ANALYSIS_DELAY_SECONDS: - self.analyses = [] - self._report_compilation_progress() - for compilation_target in self.compilation_targets: - analyzed_successfully = True - compilation: Optional[CryticCompile] = None - analysis = None - analysis_error = None - detector_results = None - try: - # Compile our target - compilation = self._compile_target(compilation_target) - - # Create our analysis. - analysis = Slither(compilation) - - # Run detectors and obtain results - detector_classes, _ = get_detectors_and_printers() - _, detector_results, _, _ = process_detectors_and_printers(analysis, detector_classes, - []) - # Parse detector results - if detector_results is not None and isinstance(detector_results, list): - detector_results = [ - SlitherDetectorResult.from_dict(detector_result) - for detector_result in detector_results - ] - else: - detector_results = None - - except Exception as err: - # If we encounter an error, set our status. - analyzed_successfully = False - analysis_error = err - - # Add our analysis - self.analyses.append( - AnalysisResult( - succeeded=analyzed_successfully, - compilation_target=compilation_target, - compilation=compilation, - analysis=analysis, - error=analysis_error, - detector_results=detector_results - ) - ) - - # Report analysis status to our client - self._report_compilation_progress() - - # Refresh our detector results - self._refresh_detector_output() - - # Clear the dirty status which triggers analysis in later polling rounds. - self._analysis_last_change_time = None - - def generate_compilation_targets(self) -> List[CompilationTarget]: - # TODO: Loop through self.solidity_files, parse files to determine which compilation buckets/parameters - # should be used. - - # TODO: Parse import strings, create remappings for unresolved imports. - # Regex: import\s+[^"]*"([^"]+)".*; - - # TODO: Parse semvers, find incompatibilities, put them into different compilation buckets - # and potentially return data about satisfactory solc versions, which may enable us to - # use solc-select to compile all. - # Regex: pragma\s+solidity\s+(.*); - - # TODO: When we've determined our compilation buckets, create multiple standard json files and return them all. - if len(self.solidity_file_uris) == 0: - return [] - - # Create our sources section of solc standard json - sources = {} - for solidity_file_uri in self.solidity_file_uris: - solidity_file_path = uri_to_fs_path(solidity_file_uri) - sources[solidity_file_path] = { - 'urls': [ - solidity_file_path - ] - } - - # Create our standard json result - target = { - 'language': 'Solidity', - 'sources': sources, - 'settings': { - 'remappings': [], - 'optimizer': { - 'enabled': False, - }, - 'outputSelection': { - '*': { - '*': [ - 'abi', - 'metadata', - 'devdoc', - 'userdoc', - 'evm.bytecode', - 'evm.deployedBytecode' - ], - '': [ - 'ast' - ] - } - } - } - } - - return [CompilationTarget( - target_type=CompilationTargetType.STANDARD_JSON, - target_basic=None, - target_standard_json=CompilationTargetStandardJson(target), - cwd_workspace=None, - crytic_compile_args=None) - ] - - def _compile_target(self, compilation_settings: CompilationTarget) -> CryticCompile: - """ - Compiles a target with the provided compilation settings using crytic-compile. - :return: Returns an instance of crytic-compile. - """ - if compilation_settings.target_type == CompilationTargetType.BASIC: - # If the target type is a basic target and we have provided settings, pass them to crytic compile. - if compilation_settings.target_basic is not None: - # Obtain our workspace folder - workspace_folder_path: Optional[str] = None - if compilation_settings.cwd_workspace is not None: - for workspace_folder in self.workspace_folders.values(): - if workspace_folder.name.lower() == compilation_settings.cwd_workspace.lower(): - workspace_folder_path = uri_to_fs_path(workspace_folder.uri) - break - - # Obtain our target. If this is a relative path, we prepend our working directory. - target_path = compilation_settings.target_basic.target - if not os.path.isabs(target_path) and workspace_folder_path is not None: - target_path = os.path.normpath(os.path.join(workspace_folder_path, target_path)) - - # Compile our target - return CryticCompile(target_path) - - elif compilation_settings.target_type == CompilationTargetType.STANDARD_JSON: - # If the target type is standard json and we have provided settings, pass them to crytic compile. - if compilation_settings.target_standard_json is not None: - # TODO: Add support for other arguments (solc_working_dir, etc) - return CryticCompile(SolcStandardJson(compilation_settings.target_standard_json.target)) - - # Raise an exception if there was no relevant exception. - raise ValueError( - f"Could not compile target type {compilation_settings.target_type.name} as insufficient settings were " - f"provided." - ) diff --git a/slither_lsp/app/types/analysis_structures.py b/slither_lsp/app/types/analysis_structures.py index 3c42b23..b39dcc3 100644 --- a/slither_lsp/app/types/analysis_structures.py +++ b/slither_lsp/app/types/analysis_structures.py @@ -1,164 +1,128 @@ -from dataclasses import dataclass, field -from enum import Enum -from typing import Optional, Union, Any, List, Dict +from typing import List, Optional +import attrs from crytic_compile import CryticCompile from slither import Slither -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure, serialization_metadata -class CompilationTargetType(Enum): - """ - Represents the type of target for compilation. - """ - BASIC = 'basic' - STANDARD_JSON = 'standard_json' - - -@dataclass -class CompilationTargetBasic(SerializableStructure): - """ - Data structure which represents options to compile against a basic string path target for crytic-compile. - """ - # The target destination for a crytic-compile target. - target: str - - -@dataclass -class CompilationTargetStandardJson(SerializableStructure): - """ - Data structure which represents options to compile against solc standard json via crytic-compile. - References: - https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description - """ - # The target destination for a crytic-compile target. - target: Any - - -@dataclass -class CompilationTarget(SerializableStructure): - """ - Data structure which represents options to compile solidity files. - """ - # Defines the type of target for compilation settings. - target_type: CompilationTargetType - - # Defines compilation settings for a BASIC target type. - target_basic: Optional[CompilationTargetBasic] = None - - # Defines compilation settings for a STANDARD_JSON target type. - target_standard_json: Optional[CompilationTargetStandardJson] = None - - # Defines an optional workspace folder name to use as the working directory. - cwd_workspace: Optional[str] = None - - # Additional arguments to provide to crytic-compile. - crytic_compile_args: Optional[Dict[str, Union[str, bool]]] = None - - -@dataclass -class SlitherDetectorSettings(SerializableStructure): +@attrs.define +class SlitherDetectorSettings: """ Data structure which represents options to show slither detector output. """ - # Defines whether detector output should be enabled at all - enabled: bool = True - # Defines a list of detector check identifiers which represent detector output we wish to suppress. - hidden_checks: List[str] = field(default_factory=list) + enabled: bool = attrs.field(default=True) + """ Defines whether detector output should be enabled at all """ + hidden_checks: List[str] = attrs.field(default=[]) + """ Defines a list of detector check identifiers which represent detector output we wish to suppress. """ -@dataclass -class SlitherDetectorResultElementSourceMapping(SerializableStructure): - # The source starting offset for this element - start: int - # The source ending offset for this element - length: int +@attrs.define +class SlitherDetectorResultElementSourceMapping: + start: int = attrs.field() + """ The source starting offset for this element """ - # An absolute path to the filename. - filename_absolute: str = field(metadata=serialization_metadata(name_override='filename_absolute')) + length: int = attrs.field() + """ The source ending offset for this element """ - # A relative path to the filename from the working directory slither was executed from. - filename_relative: str = field(metadata=serialization_metadata(name_override='filename_relative')) + filename_absolute: str = attrs.field() + """ An absolute path to the filename. """ - # A short filepath used for display purposes. - filename_short: str = field(metadata=serialization_metadata(name_override='filename_short')) + filename_relative: str = attrs.field() + """ A relative path to the filename from the working directory slither was executed from. """ - # The filename used by slither - filename_used: str = field(metadata=serialization_metadata(name_override='filename_used')) + filename_short: str = attrs.field() + """ A short filepath used for display purposes. """ - # A list of line numbers associated with the finding. - lines: List[int] + lines: List[int] = attrs.field() + """ A list of line numbers associated with the finding. """ - # The starting column of the finding, starting from the first line. - starting_column: int = field(metadata=serialization_metadata(name_override='starting_column')) + starting_column: int = attrs.field() + """ The starting column of the finding, starting from the first line. """ - # The ending column of the finding, ending on the last line. - ending_column: int = field(metadata=serialization_metadata(name_override='ending_column')) + ending_column: int = attrs.field() + """ The ending column of the finding, ending on the last line. """ + is_dependency: bool = attrs.field() -@dataclass -class SlitherDetectorResultElement(SerializableStructure): - # The name of the source mapped item associated with a slither detector result - name: str - # The source mapping associated with the element associated with the detector result. - source_mapping: Optional[SlitherDetectorResultElementSourceMapping] = field(metadata=serialization_metadata(name_override='source_mapping')) +@attrs.define +class SlitherDetectorResultElement: + name: str = attrs.field() + """ The name of the source mapped item associated with a slither detector result """ - # The type of item this represents (contract, function, etc.) - type: str + source_mapping: Optional[SlitherDetectorResultElementSourceMapping] = attrs.field() + """ The source mapping associated with the element associated with the detector result. """ - # The fields related to this element type. - type_specific_fields: Any = field(metadata=serialization_metadata(name_override='type_specific_fields')) + type: str = attrs.field() + """ The type of item this represents (contract, function, etc.) """ - # Any additional detector-specific field. - additional_fields: Any = field(metadata=serialization_metadata(name_override='additional_fields')) + @staticmethod + def from_dict(dict_): + return SlitherDetectorResultElement( + name=dict_["name"], + source_mapping=( + SlitherDetectorResultElementSourceMapping(**dict_["source_mapping"]) + if dict_["source_mapping"] + else None + ), + type=dict_["type"], + ) -@dataclass -class SlitherDetectorResult(SerializableStructure): +@attrs.define +class SlitherDetectorResult: """ Data structure which represents slither detector results. """ - # The detector check identifier. - check: str - # The level of confidence in the detector result. - confidence: str + check: str = attrs.field() + """ The detector check identifier. """ + + confidence: str = attrs.field() + """ The level of confidence in the detector result. """ - # The severity of the detector result if it is a true-positive. - impact: str + impact: str = attrs.field() + """ The severity of the detector result if it is a true-positive. """ - # A description of a detector result. - description: str + description: str = attrs.field() + """ A description of a detector result. """ - # Source mapped elements that are relevant to this detector result. - elements: List[SlitherDetectorResultElement] + elements: List[SlitherDetectorResultElement] = attrs.field() + """ Source mapped elements that are relevant to this detector result. """ - # Any additional detector-specific field. - additional_fields: Any = field(metadata=serialization_metadata(name_override='additional_fields')) + @staticmethod + def from_dict(dict_): + return SlitherDetectorResult( + check=dict_["check"], + confidence=dict_["confidence"], + impact=dict_["impact"], + description=dict_["description"], + elements=[ + SlitherDetectorResultElement.from_dict(elem) + for elem in dict_["elements"] + ], + ) -@dataclass +@attrs.define class AnalysisResult: """ Data structure which represents compilation and analysis results for internal use. """ - # Defines if our analysis succeeded - succeeded: bool - # Our compilation target settings - compilation_target: CompilationTarget + succeeded: bool = attrs.field() + """ Defines if our analysis succeeded """ - # Our compilation result - compilation: Optional[CryticCompile] + compilation: Optional[CryticCompile] = attrs.field() + """ Our compilation result """ - # Our analysis result - analysis: Optional[Slither] + analysis: Optional[Slither] = attrs.field() + """ Our analysis result """ - # An exception if the analysis did not succeed - error: Optional[Exception] + error: Optional[Exception] = attrs.field() + """ An exception if the analysis did not succeed """ - # Detector output - detector_results: Optional[List[SlitherDetectorResult]] = None + detector_results: Optional[List[SlitherDetectorResult]] = attrs.field(default=None) + """ Detector output """ diff --git a/slither_lsp/app/types/params.py b/slither_lsp/app/types/params.py index 6fe0b46..b7814fc 100644 --- a/slither_lsp/app/types/params.py +++ b/slither_lsp/app/types/params.py @@ -1,38 +1,35 @@ -from dataclasses import dataclass, field -from typing import Optional, List +from typing import List, Optional, Union -from slither_lsp.app.types.analysis_structures import CompilationTarget -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure, serialization_metadata +import attrs +from slither_lsp.app.types.analysis_structures import SlitherDetectorSettings -@dataclass -class SetCompilationTargetsParams(SerializableStructure): - """ - Data structure which represents parameters used to set compilation targets - """ - # Represents the list of compilation targets to compile and analyze. If empty, auto-compilation will be used. - targets: List[CompilationTarget] +@attrs.define +class AnalysisRequestParams: + uris: Optional[List[str]] = attrs.field() -@dataclass -class AnalysisResultProgress(SerializableStructure): - """ - Data structure which represents an individual compilation and analysis result which is sent to a client. - """ - # Defines if our analysis succeeded. If None/null, indicates analysis is still pending. - succeeded: Optional[bool] = field(metadata=serialization_metadata(include_none=True)) +SLITHER_ANALYZE = "$/slither/analyze" +SLITHER_GET_DETECTOR_LIST = "$/slither/getDetectorList" +SLITHER_GET_VERSION = "$/slither/getVersion" +SLITHER_SET_DETECTOR_SETTINGS = "$/slither/setDetectorSettings" +CRYTIC_COMPILE_GET_COMMAND_LINE_ARGUMENTS = "$/cryticCompile/getCommandLineArguments" - # Our compilation target settings - compilation_target: CompilationTarget - # An exception if the operation did not succeed - error: Optional[str] +@attrs.define +class SetDetectorSettingsRequest: + id: Union[int, str] = attrs.field() + params: SlitherDetectorSettings = attrs.field() + method: str = SLITHER_SET_DETECTOR_SETTINGS + jsonrpc: str = attrs.field(default="2.0") -@dataclass -class AnalysisProgressParams(SerializableStructure): - """ - Data structure which represents compilation and analysis results which are communicated to the client. - """ - # A list of analysis results, one for each compilation target. - results: List[AnalysisResultProgress] +METHOD_TO_TYPES = { + # Requests + SLITHER_SET_DETECTOR_SETTINGS: ( + SetDetectorSettingsRequest, + None, + SlitherDetectorSettings, + None, + ), +} diff --git a/slither_lsp/app/utils/file_paths.py b/slither_lsp/app/utils/file_paths.py index e09f97c..0fa94b4 100644 --- a/slither_lsp/app/utils/file_paths.py +++ b/slither_lsp/app/utils/file_paths.py @@ -5,8 +5,8 @@ def is_solidity_file(path: str) -> bool: - filename_base, file_extension = os.path.splitext(path) - return file_extension is not None and file_extension.lower() == '.sol' + _, file_extension = os.path.splitext(path) + return file_extension is not None and file_extension.lower() == ".sol" def uri_to_fs_path(uri: str) -> str: @@ -19,7 +19,7 @@ def normalize_uri(uri: str) -> str: def fs_path_to_uri(path: str) -> str: - uri = urljoin('file:', pathname2url(path)) + uri = urljoin("file:", pathname2url(path)) return uri @@ -41,10 +41,8 @@ def get_solidity_files(folders: Iterable[str], recursive=True) -> Set[str]: solidity_files.add(full_path) elif recursive and os.path.isdir(full_path): # If recursive, join our set with any other discovered files in subdirectories. - if item != 'node_modules': - solidity_files.update( - get_solidity_files([full_path], recursive) - ) + if item != "node_modules": + solidity_files.update(get_solidity_files([full_path], recursive)) # Return all discovered solidity files return solidity_files diff --git a/slither_lsp/app/utils/ranges.py b/slither_lsp/app/utils/ranges.py new file mode 100644 index 0000000..c28dac8 --- /dev/null +++ b/slither_lsp/app/utils/ranges.py @@ -0,0 +1,64 @@ +from typing import Union + +import lsprotocol.types as lsp +from crytic_compile.crytic_compile import CryticCompile +from slither.core.declarations import ( + Contract, + Enum, + Event, + Function, + Structure, +) +from slither.core.source_mapping.source_mapping import Source +from slither.utils.source_mapping import get_definition +from slither_lsp.app.utils.file_paths import fs_path_to_uri + + +def source_to_range(source: Source) -> lsp.Range: + """ + Converts a slither Source mapping object into a Language Server Protocol Location. + :param source: The slither Source mapping object to convert into a Location. + :return: Returns a Location representing the slither Source mapping object. + """ + + # Otherwise we can return a location fairly easily. + return lsp.Range( + start=lsp.Position( + line=source.lines[0] - 1, + character=max(0, source.starting_column - 1), + ), + end=lsp.Position( + line=source.lines[-1] - 1, + character=max(0, source.ending_column - 1), + ), + ) + + +def source_to_location(source: Source) -> lsp.Location: + """ + Converts a slither Source mapping object into a Language Server Protocol Location. + :param source: The slither Source mapping object to convert into a Location. + :return: Returns a Location representing the slither Source mapping object. + """ + + # Otherwise we can return a location fairly easily. + return lsp.Location( + uri=fs_path_to_uri(source.filename.absolute), + range=source_to_range(source), + ) + + +def get_object_name_range( + obj: Union[Function, Contract, Enum, Event, Structure], comp: CryticCompile +) -> lsp.Range: + name_pos = get_definition(obj, comp) + return lsp.Range( + start=lsp.Position( + line=name_pos.lines[0] - 1, + character=name_pos.starting_column - 1, + ), + end=lsp.Position( + line=name_pos.lines[0] - 1, + character=name_pos.starting_column + len(obj.name) - 1, + ), + ) diff --git a/slither_lsp/lsp/__init__.py b/slither_lsp/lsp/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/io/__init__.py b/slither_lsp/lsp/io/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/io/event_emitter.py b/slither_lsp/lsp/io/event_emitter.py deleted file mode 100644 index dc4dfec..0000000 --- a/slither_lsp/lsp/io/event_emitter.py +++ /dev/null @@ -1,24 +0,0 @@ -from threading import Thread -from typing import Any - -from pymitter import EventEmitter - - -class AsyncEventEmitter: - def __init__(self): - self._emitter = EventEmitter() - - def emit(self, *args, **kwargs): - return self._emitter.emit(*args, **kwargs) - - def on(self, event: Any, func: Any, ttl: int = -1, asynchronous: bool = True): - # Determine if we want to handle this synchronously or asynchronously - if asynchronous: - # Create a function which wraps our function in a new thread - def wrapped_func(*args, **kwargs): - thread = Thread(target=func, args=args, kwargs=kwargs) - thread.start() - - return self._emitter.on(event=event, func=wrapped_func, ttl=ttl) - else: - return self._emitter.on(event=event, func=func, ttl=ttl) diff --git a/slither_lsp/lsp/io/jsonrpc_io.py b/slither_lsp/lsp/io/jsonrpc_io.py deleted file mode 100644 index d526c17..0000000 --- a/slither_lsp/lsp/io/jsonrpc_io.py +++ /dev/null @@ -1,101 +0,0 @@ -import json -import re -from io import BufferedIOBase -from threading import Lock -from typing import Any, Optional, BinaryIO, Tuple, List, Union, IO - -# The regex used to parse Content-Length from the JSON-RPC messages. -CONTENT_LENGTH_REGEX = re.compile(r"Content-Length:\s+(\d+)\s*", re.IGNORECASE) - - -class JsonRpcIo: - """ - Provides IO for Language Server Protocol JSON-RPC over generic file handles. - - Target: Language Server Protocol 3.16 - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/ - """ - def __init__(self, read_file_handle: IO, write_file_handle: IO): - self._read_file_handle = read_file_handle - self._write_file_handle = write_file_handle - self._read_lock = Lock() - self._write_lock = Lock() - - def read(self) -> Union[Tuple[List[str], Any], None]: - """ - Attempts to read a message from the underlying file handle and deserialize it. - :return: Returns a tuple(headers, body) if a message was available, returns None otherwise. - """ - # TODO: We'll likely want to add some proper exception handling logic here. - # TODO: We'll probably want to parse and read based off of the provided Content-Type for future flexibility. - with self._read_lock: - # Attempt to read headers, searching for Content-Length in the process - headers = [] - content_length = None - while True: - # Read a line, stop reading if one was not available - line = self._read_file_handle.readline() - if line is None: - break - - # If stripping it results in no remaining content, then it should be the final \r\n string. - if isinstance(line, bytes): - line = line.decode('utf-8') - if not line.strip(): - break - - # Add the line to our list of headers - headers.append(line) - - # See if this line is the content-length header - match = CONTENT_LENGTH_REGEX.match(line) - if match: - match = match.group(1) - if len(match) > 0: - assert content_length is None, "More than one Content-Length header should not be received." - content_length = int(match) - - # If we didn't receive a content-length header, return None and try to skip - # TODO: We probably want to send the appropriate error code back in the future. - if content_length is None: - return None - - # Next we'll want to read our body - body = self._read_file_handle.read(content_length) - if isinstance(body, bytes): - body = body.decode('utf-8') - return headers, json.loads(body) - - def write(self, data: Any) -> None: - """ - Serializes the provided data as JSON and sends it over the underlying file handle. - :param data: The object to serialize as JSON and write to the underlying file handle. - :return: None - """ - # TODO: We'll likely want to add some exception handling logic here. - with self._write_lock: - # Determine if this is a binary stream - is_binary_stream = isinstance(self._write_file_handle, (BinaryIO, BufferedIOBase)) - - # Get our data as JSON. Depending on if the stream is text/binary, we use encoded the data - json_data = json.dumps(data) - encoded_json_data = json_data.encode('utf-8') - if is_binary_stream: - json_data = encoded_json_data - - # Construct our headers. Headers are delimited by '\r\n'. This is also the case for the headers+body. - # Note: Although Content-Type defaults to the value below, we remain explicit to be safe. - headers = ( - f"Content-Length: {len(encoded_json_data)}\r\n" - "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n" - "\r\n" - ) - - # Depending on if the stream is text/binary, we encode the data - if is_binary_stream: - headers = headers.encode('utf-8') - - # Write our data and flush it to our handle - self._write_file_handle.write(headers + json_data) - self._write_file_handle.flush() diff --git a/slither_lsp/lsp/request_handlers/__init__.py b/slither_lsp/lsp/request_handlers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/request_handlers/base_handler.py b/slither_lsp/lsp/request_handlers/base_handler.py deleted file mode 100644 index 129df74..0000000 --- a/slither_lsp/lsp/request_handlers/base_handler.py +++ /dev/null @@ -1,25 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any - -from slither_lsp.lsp.state.server_context import ServerContext - - -class BaseRequestHandler(ABC): - """ - Represents a handler for a request or notification provided over the Language Server Protocol. - """ - - @staticmethod - @abstractmethod - def method_name(): - """ - The name of the method which this request/notification handler handles. This should be unique per - handler. Handlers which are custom or client/server-specific should be prefixed with '$/' - :return: The name of the method which this request/notification handler handles. - """ - pass - - @staticmethod - @abstractmethod - def process(context: ServerContext, params: Any) -> Any: - pass diff --git a/slither_lsp/lsp/request_handlers/lifecycle/__init__.py b/slither_lsp/lsp/request_handlers/lifecycle/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/request_handlers/lifecycle/exit.py b/slither_lsp/lsp/request_handlers/lifecycle/exit.py deleted file mode 100644 index 6d38ebd..0000000 --- a/slither_lsp/lsp/request_handlers/lifecycle/exit.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext - - -class ExitHandler(BaseRequestHandler): - """ - Handler for the 'exit' notification, which signals that the language server should shut down. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#exit - """ - method_name = "exit" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - - # TODO: Find a more elegant way to exit and make sure all important operations were torn down. - import sys - sys.exit(0) diff --git a/slither_lsp/lsp/request_handlers/lifecycle/initialize.py b/slither_lsp/lsp/request_handlers/lifecycle/initialize.py deleted file mode 100644 index f4e4064..0000000 --- a/slither_lsp/lsp/request_handlers/lifecycle/initialize.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.basic_structures import WorkspaceFolder -from slither_lsp.lsp.types.params import InitializeParams, InitializeResult - - -class InitializeHandler(BaseRequestHandler): - """ - Handler for the 'initialize' request, which exchanges capability/workspace information. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#initialize - """ - method_name = "initialize" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - # Parse our initialization params - params: InitializeParams = InitializeParams.from_dict(params) - - # Store some information in our server context - context.client_info = params.client_info - context.trace = params.trace - - # Parse client capabilities - context.client_capabilities = params.capabilities - - # Create our initialization result - result = InitializeResult(context.server_capabilities, context.server_info) - - # Set our server as initialized, trigger relevant event - context.server_initialized = True - context.event_emitter.emit( - 'server.initialized', - params=params, - result=result - ) - - # Return our server capabilities - return result.to_dict() diff --git a/slither_lsp/lsp/request_handlers/lifecycle/initialized.py b/slither_lsp/lsp/request_handlers/lifecycle/initialized.py deleted file mode 100644 index f19baa6..0000000 --- a/slither_lsp/lsp/request_handlers/lifecycle/initialized.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext - - -class InitializedHandler(BaseRequestHandler): - """ - Handler for the 'initialized' notification, which notifies the server that the client successfully initialized. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#initialized - """ - method_name = "initialized" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - - # Set our context into an initialized state. - context.client_initialized = True - context.event_emitter.emit('client.initialized') - - # Notifications do not return a response - return None diff --git a/slither_lsp/lsp/request_handlers/lifecycle/set_trace.py b/slither_lsp/lsp/request_handlers/lifecycle/set_trace.py deleted file mode 100644 index 98faf38..0000000 --- a/slither_lsp/lsp/request_handlers/lifecycle/set_trace.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import SetTraceParams - - -class SetTraceHandler(BaseRequestHandler): - """ - Handler for the '$/setTrace' notification, which sets the verbosity level of log traces on the server. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#setTrace - """ - method_name = "$/setTrace" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - # Parse our initialization params - params: SetTraceParams = SetTraceParams.from_dict(params) - - # Set our value and emit a relevant event. - context.trace = params.value - context.event_emitter.emit( - 'trace.set', - params=params - ) - - # Notifications do not return a response - return None diff --git a/slither_lsp/lsp/request_handlers/lifecycle/shutdown.py b/slither_lsp/lsp/request_handlers/lifecycle/shutdown.py deleted file mode 100644 index b697467..0000000 --- a/slither_lsp/lsp/request_handlers/lifecycle/shutdown.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext - - -class ShutdownHandler(BaseRequestHandler): - """ - Handler for the 'shutdown' request, which prepares the server for a subsequent exit. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#shutdown - """ - method_name = "shutdown" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - # Set the server context into a shutdown state, so it should not process anything heavily. - # TODO: Create and trigger an event for this + ensure other code uses this state to not perform any meaningful - # updates. - context.shutdown = True - - # TODO: We exit here, but we should not, that should happen with the 'exit' notification, but sometimes - # we don't get an 'exit' notification for some reason, so the language server never exits. This - # should be investigated. - import sys - sys.exit(0) - - # The return value on success is null. - return None diff --git a/slither_lsp/lsp/request_handlers/registered_handlers.py b/slither_lsp/lsp/request_handlers/registered_handlers.py deleted file mode 100644 index 76bd671..0000000 --- a/slither_lsp/lsp/request_handlers/registered_handlers.py +++ /dev/null @@ -1,33 +0,0 @@ -# pylint: disable=unused-import,relative-beyond-top-level -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.request_handlers.lifecycle.initialize import InitializeHandler -from slither_lsp.lsp.request_handlers.lifecycle.initialized import InitializedHandler -from slither_lsp.lsp.request_handlers.lifecycle.shutdown import ShutdownHandler -from slither_lsp.lsp.request_handlers.lifecycle.exit import ExitHandler -from slither_lsp.lsp.request_handlers.lifecycle.set_trace import SetTraceHandler - -# workspace -from slither_lsp.lsp.request_handlers.workspace.did_change_workspace_folders import DidChangeWorkspaceFolderHandler -from slither_lsp.lsp.request_handlers.workspace.did_change_watched_files import DidChangeWatchedFilesHandler -from slither_lsp.lsp.request_handlers.workspace.will_create_files import WillCreateFilesHandler -from slither_lsp.lsp.request_handlers.workspace.did_create_files import DidCreateFilesHandler -from slither_lsp.lsp.request_handlers.workspace.will_rename_files import WillRenameFilesHandler -from slither_lsp.lsp.request_handlers.workspace.did_rename_files import DidRenameFilesHandler -from slither_lsp.lsp.request_handlers.workspace.will_delete_files import WillDeleteFilesHandler -from slither_lsp.lsp.request_handlers.workspace.did_delete_files import DidDeleteFilesHandler - - -# text synchronization -from slither_lsp.lsp.request_handlers.text_document.did_open import DidOpenHandler -from slither_lsp.lsp.request_handlers.text_document.did_change import DidChangeHandler -from slither_lsp.lsp.request_handlers.text_document.will_save import WillSaveHandler -from slither_lsp.lsp.request_handlers.text_document.did_save import DidSaveHandler -from slither_lsp.lsp.request_handlers.text_document.did_close import DidCloseHandler - -# language features -from slither_lsp.lsp.request_handlers.text_document.hover import HoverHandler -from slither_lsp.lsp.request_handlers.text_document.goto_declaration import GoToDeclarationHandler -from slither_lsp.lsp.request_handlers.text_document.goto_definition import GoToDefinitionHandler -from slither_lsp.lsp.request_handlers.text_document.goto_type_definition import GoToTypeDefinitionHandler -from slither_lsp.lsp.request_handlers.text_document.goto_implementation import GoToImplementationHandler -from slither_lsp.lsp.request_handlers.text_document.find_references import FindReferencesHandler diff --git a/slither_lsp/lsp/request_handlers/text_document/__init__.py b/slither_lsp/lsp/request_handlers/text_document/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/request_handlers/text_document/did_change.py b/slither_lsp/lsp/request_handlers/text_document/did_change.py deleted file mode 100644 index 67a38ee..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/did_change.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import DidChangeTextDocumentParams - - -class DidChangeHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/didChange' notification, which signals changes made to text documents. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didChange - """ - method_name = "textDocument/didChange" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/didChange' notification, which signals changes made to text documents. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didChange - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Parse our params - params: DidChangeTextDocumentParams = DidChangeTextDocumentParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.didChange', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/text_document/did_close.py b/slither_lsp/lsp/request_handlers/text_document/did_close.py deleted file mode 100644 index 8a249c4..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/did_close.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import DidCloseTextDocumentParams - - -class DidCloseHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/didClose' notification, which signals closed text documents. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didClose - """ - method_name = "textDocument/didClose" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/didClose' notification, which signals closed text documents. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didClose - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Parse our params - params: DidCloseTextDocumentParams = DidCloseTextDocumentParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.didClose', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/text_document/did_open.py b/slither_lsp/lsp/request_handlers/text_document/did_open.py deleted file mode 100644 index a1d17a8..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/did_open.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import DidOpenTextDocumentParams - - -class DidOpenHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/didOpen' notification, which signals newly opened text documents. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didOpen - """ - method_name = "textDocument/didOpen" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/didOpen' notification, which signals newly opened text documents. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didOpen - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Parse our params - params: DidOpenTextDocumentParams = DidOpenTextDocumentParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.didOpen', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/text_document/did_save.py b/slither_lsp/lsp/request_handlers/text_document/did_save.py deleted file mode 100644 index 1e5b60f..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/did_save.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import DidSaveTextDocumentParams - - -class DidSaveHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/didSave' notification, which signals a text document was saved. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didSave - """ - method_name = "textDocument/didSave" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/didSave' notification, which signals a text document was saved. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didSave - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Parse our params - params: DidSaveTextDocumentParams = DidSaveTextDocumentParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.didSave', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/text_document/find_references.py b/slither_lsp/lsp/request_handlers/text_document/find_references.py deleted file mode 100644 index f7be7c9..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/find_references.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import ReferenceParams - - -class FindReferencesHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/references' request, which resolves project-wide references for the symbol denoted - by the given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_references - """ - method_name = "textDocument/references" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/references' request, which resolves project-wide references for the symbol denoted - by the given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_references - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location[] | None - """ - # Parse our params - params: ReferenceParams = ReferenceParams.from_dict(params) - - # Define our result - result = None - - # If we have a hook, call it - if context.server_hooks is not None: - result = context.server_hooks.find_references(context, params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.references', - params=params, - result=result - ) - - # Serialize our result if we have one. - if result is not None: - result = [location.to_dict() for location in result] - - return result diff --git a/slither_lsp/lsp/request_handlers/text_document/goto_declaration.py b/slither_lsp/lsp/request_handlers/text_document/goto_declaration.py deleted file mode 100644 index 7ed1b2b..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/goto_declaration.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure -from slither_lsp.lsp.types.params import DeclarationParams - - -class GoToDeclarationHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/declaration' request, which resolves a declaration location of a symbol at a - given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_declaration - """ - method_name = "textDocument/declaration" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/declaration' request and attempts to resolve a declaration location of a symbol at a - given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_declaration - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location | Location[] | LocationLink[] | None - """ - # Parse our params - params: DeclarationParams = DeclarationParams.from_dict(params) - - # Define our result - result = None - - # If we have a hook, call it - if context.server_hooks is not None: - result = context.server_hooks.goto_declaration(context, params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.declaration', - params=params, - result=result - ) - - # Serialize our result depending on the type. - if result is not None: - if isinstance(result, SerializableStructure): - result = result.to_dict() - elif isinstance(result, list): - result = [ - result_element.to_dict() if isinstance(result_element, SerializableStructure) else result_element - for result_element in result - ] - - return result diff --git a/slither_lsp/lsp/request_handlers/text_document/goto_definition.py b/slither_lsp/lsp/request_handlers/text_document/goto_definition.py deleted file mode 100644 index d6c4ea8..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/goto_definition.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure -from slither_lsp.lsp.types.params import DefinitionParams - - -class GoToDefinitionHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/definition' request, which resolves a definition location of a symbol at a - given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_definition - """ - method_name = "textDocument/definition" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/definition' request and attempts to resolve a definition location of a symbol at a - given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_definition - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location | Location[] | LocationLink[] | None - """ - # Parse our params - params: DefinitionParams = DefinitionParams.from_dict(params) - - # Define our result - result = None - - # If we have a hook, call it - if context.server_hooks is not None: - result = context.server_hooks.goto_definition(context, params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.definition', - params=params, - result=result - ) - - # Serialize our result depending on the type. - if result is not None: - if isinstance(result, SerializableStructure): - result = result.to_dict() - elif isinstance(result, list): - result = [ - result_element.to_dict() if isinstance(result_element, SerializableStructure) else result_element - for result_element in result - ] - - return result diff --git a/slither_lsp/lsp/request_handlers/text_document/goto_implementation.py b/slither_lsp/lsp/request_handlers/text_document/goto_implementation.py deleted file mode 100644 index 996cbc9..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/goto_implementation.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure -from slither_lsp.lsp.types.params import ImplementationParams - - -class GoToImplementationHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/implementation' request, which resolves a implementation location of a symbol at a - given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_implementation - """ - method_name = "textDocument/implementation" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/implementation' request and attempts to resolve a implementation location of a symbol - at a given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_implementation - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location | Location[] | LocationLink[] | None - """ - # Parse our params - params: ImplementationParams = ImplementationParams.from_dict(params) - - # Define our result - result = None - - # If we have a hook, call it - if context.server_hooks is not None: - result = context.server_hooks.goto_implementation(context, params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.implementation', - params=params, - result=result - ) - - # Serialize our result depending on the type. - if result is not None: - if isinstance(result, SerializableStructure): - result = result.to_dict() - elif isinstance(result, list): - result = [ - result_element.to_dict() if isinstance(result_element, SerializableStructure) else result_element - for result_element in result - ] - - return result diff --git a/slither_lsp/lsp/request_handlers/text_document/goto_type_definition.py b/slither_lsp/lsp/request_handlers/text_document/goto_type_definition.py deleted file mode 100644 index 1dec326..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/goto_type_definition.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure -from slither_lsp.lsp.types.params import TypeDefinitionParams - - -class GoToTypeDefinitionHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/typeDefinition' request, which resolves a type definition location of a symbol at a - given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_typeDefinition - """ - method_name = "textDocument/typeDefinition" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/typeDefinition' request and attempts to resolve a type definition location of a symbol - at a given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_typeDefinition - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location | Location[] | LocationLink[] | None - """ - # Parse our params - params: TypeDefinitionParams = TypeDefinitionParams.from_dict(params) - - # Define our result - result = None - - # If we have a hook, call it - if context.server_hooks is not None: - result = context.server_hooks.goto_type_definition(context, params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.typeDefinition', - params=params, - result=result - ) - - # Serialize our result depending on the type. - if result is not None: - if isinstance(result, SerializableStructure): - result = result.to_dict() - elif isinstance(result, list): - result = [ - result_element.to_dict() if isinstance(result_element, SerializableStructure) else result_element - for result_element in result - ] - - return result diff --git a/slither_lsp/lsp/request_handlers/text_document/hover.py b/slither_lsp/lsp/request_handlers/text_document/hover.py deleted file mode 100644 index b662a7d..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/hover.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Any, Optional - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure -from slither_lsp.lsp.types.params import DeclarationParams, HoverParams, Hover - - -class HoverHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/hover' request, which resolves hover information at a given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_hover - """ - method_name = "textDocument/hover" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/hover' request, which resolves hover information at a given text document position. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_hover - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Hover | None - """ - # Parse our params - params: HoverParams = HoverParams.from_dict(params) - - # Define our result - result = None - - # If we have a hook, call it - if context.server_hooks is not None: - result = context.server_hooks.hover(context, params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.hover', - params=params, - result=result - ) - - # Serialize our result depending on the type. - if result is not None: - if isinstance(result, SerializableStructure): - result = result.to_dict() - - return result diff --git a/slither_lsp/lsp/request_handlers/text_document/will_save.py b/slither_lsp/lsp/request_handlers/text_document/will_save.py deleted file mode 100644 index ad9404b..0000000 --- a/slither_lsp/lsp/request_handlers/text_document/will_save.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import WillSaveTextDocumentParams - - -class WillSaveHandler(BaseRequestHandler): - """ - Handler for the 'textDocument/willSave' notification, which signals a text document is about to be saved. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_willSave - """ - method_name = "textDocument/willSave" - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'textDocument/willSave' notification, which signals a text document is about to be saved. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_willSave - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Parse our params - params: WillSaveTextDocumentParams = WillSaveTextDocumentParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'textDocument.willSave', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/workspace/__init__.py b/slither_lsp/lsp/request_handlers/workspace/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/request_handlers/workspace/did_change_watched_files.py b/slither_lsp/lsp/request_handlers/workspace/did_change_watched_files.py deleted file mode 100644 index 03a06b4..0000000 --- a/slither_lsp/lsp/request_handlers/workspace/did_change_watched_files.py +++ /dev/null @@ -1,87 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.requests.client.register_capability import RegisterCapabilityRequest -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.types.params import DidChangeWatchedFilesParams, RegistrationParams, Registration, Unregistration -from slither_lsp.lsp.types.registration_options import DidChangeWatchedFilesRegistrationOptions - - -class DidChangeWatchedFilesHandler(BaseRequestHandler): - """ - Handler for the 'workspace/didChangeWatchedFiles' notification, which notifies the server that the client - detected changes to files which the server previously requested be watched. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didChangeWatchedFiles - """ - - method_name = "workspace/didChangeWatchedFiles" - - @classmethod - def get_registration(cls, context: ServerContext, registration_id: str, - registration_options: DidChangeWatchedFilesRegistrationOptions) -> Registration: - """ - Obtains an Registration object to be used with a 'client/registerCapability' request given a set of - registration options. - :param context: The server context which determines the server to use to send the message. - :param registration_id: The identifier to assign this capability registration to, to be used for later - unregistration. - :param registration_options: The options to provide for this capability registration. - :return: The Registration object. - """ - # Verify we have appropriate capabilities. - if context.client_capabilities.workspace is None or \ - context.client_capabilities.workspace.did_change_watched_files is None or \ - context.client_capabilities.workspace.did_change_watched_files.dynamic_registration is not True: - raise CapabilitiesNotSupportedError( - request_or_handler=cls, - additional_text="Could not dynamically register for these capabilities because the client does not " - "support dynamic registration." - ) - - return Registration(id=registration_id, method=cls.method_name, register_options=registration_options) - - @classmethod - def get_unregistration(cls, context: ServerContext, registration_id: str) -> Unregistration: - """ - Obtains an Unregistration object given a previously registered capability registration id. - :param context: The server context which determines the server to use to send the message. - :param registration_id: A previously registered capability registration id. - :return: The Unregistration to be used with a 'client/unregisterCapability' request. - """ - # Verify we have appropriate capabilities. - if context.client_capabilities.workspace is None or \ - context.client_capabilities.workspace.did_change_watched_files is None or \ - context.client_capabilities.workspace.did_change_watched_files.dynamic_registration is not True: - raise CapabilitiesNotSupportedError( - request_or_handler=cls, - additional_text="Could not dynamically register for these capabilities because the client does not " - "support dynamic registration." - ) - - return Unregistration(id=registration_id, method=cls.method_name) - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'workspace/didChangeWatchedFiles' notification which notifies the server that the client - detected changes to files which the server previously requested be watched. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didChangeWatchedFiles - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - - # Validate the structure of our request - params: DidChangeWatchedFilesParams = DidChangeWatchedFilesParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'workspace.didChangeWatchedFiles', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/workspace/did_change_workspace_folders.py b/slither_lsp/lsp/request_handlers/workspace/did_change_workspace_folders.py deleted file mode 100644 index 05ecdf2..0000000 --- a/slither_lsp/lsp/request_handlers/workspace/did_change_workspace_folders.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import DidChangeWorkspaceFoldersParams - - -class DidChangeWorkspaceFolderHandler(BaseRequestHandler): - """ - Handler for the 'workspace/didChangeWorkspaceFolders' notification, which notifies the server that the client - added or removed workspace folders. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didChangeWorkspaceFolders - """ - - method_name = "workspace/didChangeWorkspaceFolders" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - - server_supported: bool = context.server_capabilities.workspace and \ - context.server_capabilities.workspace.workspace_folders and \ - context.server_capabilities.workspace.workspace_folders.supported - if not server_supported: - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'workspace/didChangeWorkspaceFolders' notification which indicates that workspace folders were added - or removed. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didChangeWorkspaceFolders - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Verify we have appropriate capabilities - cls._check_capabilities(context) - - # Validate the structure of our request - params: DidChangeWorkspaceFoldersParams = DidChangeWorkspaceFoldersParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'workspace.didChangeWorkspaceFolders', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/workspace/did_create_files.py b/slither_lsp/lsp/request_handlers/workspace/did_create_files.py deleted file mode 100644 index 81ef71e..0000000 --- a/slither_lsp/lsp/request_handlers/workspace/did_create_files.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.types.params import CreateFilesParams - - -class DidCreateFilesHandler(BaseRequestHandler): - """ - Handler for the 'workspace/didCreateFiles' notification, which is sent from the client to the server when files - were created from within the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didCreateFiles - """ - - method_name = "workspace/didCreateFiles" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - - server_supported: bool = context.server_capabilities.workspace and \ - context.server_capabilities.workspace.file_operations and \ - context.server_capabilities.workspace.file_operations.did_create is not None - if not server_supported: - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'workspace/didCreateFiles' notification which is sent from the client to the server when files were - created from within the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didCreateFiles - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Verify we have appropriate capabilities - cls._check_capabilities(context) - - # Validate the structure of our request - params: CreateFilesParams = CreateFilesParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'workspace.didCreateFiles', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/workspace/did_delete_files.py b/slither_lsp/lsp/request_handlers/workspace/did_delete_files.py deleted file mode 100644 index f45a94d..0000000 --- a/slither_lsp/lsp/request_handlers/workspace/did_delete_files.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.types.params import DeleteFilesParams - - -class DidDeleteFilesHandler(BaseRequestHandler): - """ - Handler for the 'workspace/didDeleteFiles' notification, which is sent from the client to the server when files - were deleted from within the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didDeleteFiles - """ - - method_name = "workspace/didDeleteFiles" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - - server_supported: bool = context.server_capabilities.workspace and \ - context.server_capabilities.workspace.file_operations and \ - context.server_capabilities.workspace.file_operations.did_delete is not None - if not server_supported: - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'workspace/didDeleteFiles' notification which is sent from the client to the server when files were - deleted from within the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didDeleteFiles - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Verify we have appropriate capabilities - cls._check_capabilities(context) - - # Validate the structure of our request - params: DeleteFilesParams = DeleteFilesParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'workspace.didDeleteFiles', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/workspace/did_rename_files.py b/slither_lsp/lsp/request_handlers/workspace/did_rename_files.py deleted file mode 100644 index de5fc8c..0000000 --- a/slither_lsp/lsp/request_handlers/workspace/did_rename_files.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.types.params import RenameFilesParams - - -class DidRenameFilesHandler(BaseRequestHandler): - """ - Handler for the 'workspace/didRenameFiles' notification, which is sent from the client to the server when files - were renamed from within the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didRenameFiles - """ - - method_name = "workspace/didRenameFiles" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - - server_supported: bool = context.server_capabilities.workspace and \ - context.server_capabilities.workspace.file_operations and \ - context.server_capabilities.workspace.file_operations.did_rename is not None - if not server_supported: - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'workspace/didRenameFiles' notification which is sent from the client to the server when files were - renamed from within the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_didRenameFiles - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Verify we have appropriate capabilities - cls._check_capabilities(context) - - # Validate the structure of our request - params: RenameFilesParams = RenameFilesParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'workspace.didRenameFiles', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/workspace/will_create_files.py b/slither_lsp/lsp/request_handlers/workspace/will_create_files.py deleted file mode 100644 index b48468e..0000000 --- a/slither_lsp/lsp/request_handlers/workspace/will_create_files.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.types.params import CreateFilesParams - - -class WillCreateFilesHandler(BaseRequestHandler): - """ - Handler for the 'workspace/willCreateFiles' notification, which is sent from the client to the server before files - are actually created as long as the creation is triggered from within the client either by a user action or by - applying a workspace edit. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_willCreateFiles - """ - - method_name = "workspace/willCreateFiles" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - - server_supported: bool = context.server_capabilities.workspace and \ - context.server_capabilities.workspace.file_operations and \ - context.server_capabilities.workspace.file_operations.will_create is not None - if not server_supported: - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'workspace/willCreateFiles' notification which is sent from the client to the server before files are - actually created as long as the creation is triggered from within the client either by a user action or by - applying a workspace edit. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_willCreateFiles - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Verify we have appropriate capabilities - cls._check_capabilities(context) - - # Validate the structure of our request - params: CreateFilesParams = CreateFilesParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'workspace.willCreateFiles', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/workspace/will_delete_files.py b/slither_lsp/lsp/request_handlers/workspace/will_delete_files.py deleted file mode 100644 index d612d05..0000000 --- a/slither_lsp/lsp/request_handlers/workspace/will_delete_files.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.types.params import DeleteFilesParams - - -class WillDeleteFilesHandler(BaseRequestHandler): - """ - Handler for the 'workspace/willDeleteFiles' notification, which is sent from the client to the server before files - are actually deleted as long as the deletion is triggered from within the client either by a user action or by - applying a workspace edit. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_willDeleteFiles - """ - - method_name = "workspace/willDeleteFiles" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - - server_supported: bool = context.server_capabilities.workspace and \ - context.server_capabilities.workspace.file_operations and \ - context.server_capabilities.workspace.file_operations.will_delete is not None - if not server_supported: - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'workspace/willDeleteFiles' notification which is sent from the client to the server before files are - actually deleted as long as the deletion is triggered from within the client either by a user action or by - applying a workspace edit. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_willDeleteFiles - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Verify we have appropriate capabilities - cls._check_capabilities(context) - - # Validate the structure of our request - params: DeleteFilesParams = DeleteFilesParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'workspace.willDeleteFiles', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/request_handlers/workspace/will_rename_files.py b/slither_lsp/lsp/request_handlers/workspace/will_rename_files.py deleted file mode 100644 index 631d4f4..0000000 --- a/slither_lsp/lsp/request_handlers/workspace/will_rename_files.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Any - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.types.params import RenameFilesParams - - -class WillRenameFilesHandler(BaseRequestHandler): - """ - Handler for the 'workspace/willRenameFiles' notification, which is sent from the client to the server before files - are actually renamed as long as the rename is triggered from within the client either by a user action or by - applying a workspace edit - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_willRenameFiles - """ - - method_name = "workspace/willRenameFiles" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - - server_supported: bool = context.server_capabilities.workspace and \ - context.server_capabilities.workspace.file_operations and \ - context.server_capabilities.workspace.file_operations.will_rename is not None - if not server_supported: - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def process(cls, context: ServerContext, params: Any) -> Any: - """ - Handles a 'workspace/willRenameFiles' notification which is sent from the client to the server before files are - actually renamed as long as the rename is triggered from within the client either by a user action or by - applying a workspace edit - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_willRenameFiles - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: None - """ - # Verify we have appropriate capabilities - cls._check_capabilities(context) - - # Validate the structure of our request - params: RenameFilesParams = RenameFilesParams.from_dict(params) - - # Emit relevant events - context.event_emitter.emit( - 'workspace.willRenameFiles', - params=params - ) - - # This is a notification so we return nothing - return None diff --git a/slither_lsp/lsp/requests/__init__.py b/slither_lsp/lsp/requests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/requests/base_request.py b/slither_lsp/lsp/requests/base_request.py deleted file mode 100644 index 4f9904a..0000000 --- a/slither_lsp/lsp/requests/base_request.py +++ /dev/null @@ -1,18 +0,0 @@ -from abc import ABC, abstractmethod - - -class BaseRequest(ABC): - """ - Represents a request/notification provider for the Language Server Protocol. - Requests or Notifications should be implemented on top of this. - """ - - @staticmethod - @abstractmethod - def method_name(): - """ - The name of the method which this request/notification handler handles. This should be unique per - handler. Handlers which are custom or client/server-specific should be prefixed with '$/' - :return: The name of the method which this request/notification handler handles. - """ - pass diff --git a/slither_lsp/lsp/requests/client/register_capability.py b/slither_lsp/lsp/requests/client/register_capability.py deleted file mode 100644 index d093673..0000000 --- a/slither_lsp/lsp/requests/client/register_capability.py +++ /dev/null @@ -1,28 +0,0 @@ -from slither_lsp.lsp.requests.base_request import BaseRequest -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import RegistrationParams - - -class RegisterCapabilityRequest(BaseRequest): - """ - Request which sends a capability to a client to register for. - """ - - method_name = "client/registerCapability" - - @classmethod - def send(cls, context: ServerContext, params: RegistrationParams) -> None: - """ - Sends a 'client/registerCapability' request to the client to register for new capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability - :param context: The server context which determines the server to use to send the message. - :param params: The parameters needed to send the request. - :return: None - """ - - # Invoke the operation otherwise. - context.server.send_request_message(cls.method_name, params.to_dict()) - - # This request returns nothing on success. - return None diff --git a/slither_lsp/lsp/requests/client/unregister_capability.py b/slither_lsp/lsp/requests/client/unregister_capability.py deleted file mode 100644 index d3a6af0..0000000 --- a/slither_lsp/lsp/requests/client/unregister_capability.py +++ /dev/null @@ -1,28 +0,0 @@ -from slither_lsp.lsp.requests.base_request import BaseRequest -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import UnregistrationParams - - -class UnregisterCapabilityRequest(BaseRequest): - """ - Request which sends a capability to a client to register for. - """ - - method_name = "client/unregisterCapability" - - @classmethod - def send(cls, context: ServerContext, params: UnregistrationParams) -> None: - """ - Sends a 'client/unregisterCapability' request to the client to register for new capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability - :param context: The server context which determines the server to use to send the message. - :param params: The parameters needed to send the request. - :return: None - """ - - # Invoke the operation otherwise. - context.server.send_request_message(cls.method_name, params.to_dict()) - - # This request returns nothing on success. - return None diff --git a/slither_lsp/lsp/requests/text_document/__init__.py b/slither_lsp/lsp/requests/text_document/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/requests/text_document/publish_diagnostics.py b/slither_lsp/lsp/requests/text_document/publish_diagnostics.py deleted file mode 100644 index bda8a9c..0000000 --- a/slither_lsp/lsp/requests/text_document/publish_diagnostics.py +++ /dev/null @@ -1,39 +0,0 @@ -from slither_lsp.lsp.requests.base_request import BaseRequest -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import PublishDiagnosticsParams - - -class PublishDiagnosticsNotification(BaseRequest): - """ - Notification which sends diagnostics to the client to display. - """ - - method_name = "textDocument/publishDiagnostics" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - # Check if we have basic capabilities for this. - supported = context.client_capabilities.text_document and \ - context.client_capabilities.text_document.publish_diagnostics - if not supported: - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def send(cls, context: ServerContext, params: PublishDiagnosticsParams) -> None: - """ - Sends a 'textDocument/publishDiagnostics' request to the client to obtain workspace folders. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#window_showMessage - :param context: The server context which determines the server to use to send the message. - :param params: The parameters needed to send the request. - :return: None - """ - - # Invoke the operation otherwise. - context.server.send_notification_message(cls.method_name, params.to_dict()) diff --git a/slither_lsp/lsp/requests/window/__init__.py b/slither_lsp/lsp/requests/window/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/requests/window/log_message.py b/slither_lsp/lsp/requests/window/log_message.py deleted file mode 100644 index b60c7c7..0000000 --- a/slither_lsp/lsp/requests/window/log_message.py +++ /dev/null @@ -1,22 +0,0 @@ -from slither_lsp.lsp.requests.base_request import BaseRequest -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import LogMessageParams - - -class LogMessageNotification(BaseRequest): - """ - Notification which is sent to the client to show a message. - """ - method_name = 'window/logMessage' - - @classmethod - def send(cls, context: ServerContext, params: LogMessageParams) -> None: - """ - Sends a 'window/logMessage' notification to the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#window_logMessage - :param context: The server context which determines the server to use to send the message. - :param params: The parameters needed to send the request. - :return: None - """ - context.server.send_notification_message(cls.method_name, params.to_dict()) \ No newline at end of file diff --git a/slither_lsp/lsp/requests/window/show_document.py b/slither_lsp/lsp/requests/window/show_document.py deleted file mode 100644 index ff3858a..0000000 --- a/slither_lsp/lsp/requests/window/show_document.py +++ /dev/null @@ -1,42 +0,0 @@ -from slither_lsp.lsp.requests.base_request import BaseRequest -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import ShowDocumentParams, ShowDocumentResult - - -class ShowDocumentRequest(BaseRequest): - """ - Request which is sent to the client to display a particular document. - """ - method_name = 'window/showDocument' - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - if not (context.client_capabilities.window and context.client_capabilities.window.show_document and - context.client_capabilities.window.show_document.support): - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def send(cls, context: ServerContext, params: ShowDocumentParams) -> ShowDocumentResult: - """ - Sends a 'window/showDocument' request to the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showDocument - :param context: The server context which determines the server to use to send the message. - :param params: The parameters needed to send the request. - """ - - # Verify we have appropriate capabilities. - cls._check_capabilities(context) - - # Send the created notification. - response: dict = context.server.send_request_message(cls.method_name, params.to_dict()) - response: ShowDocumentResult = ShowDocumentResult.from_dict(response) - - # Return our result - return response diff --git a/slither_lsp/lsp/requests/window/show_message.py b/slither_lsp/lsp/requests/window/show_message.py deleted file mode 100644 index 4effc1d..0000000 --- a/slither_lsp/lsp/requests/window/show_message.py +++ /dev/null @@ -1,22 +0,0 @@ -from slither_lsp.lsp.requests.base_request import BaseRequest -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.params import ShowMessageParams - - -class ShowMessageNotification(BaseRequest): - """ - Notification which is sent to the client to show a message. - """ - method_name = 'window/showMessage' - - @classmethod - def send(cls, context: ServerContext, params: ShowMessageParams) -> None: - """ - Sends a 'window/showMessage' notification to the client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#window_showMessage - :param context: The server context which determines the server to use to send the message. - :param params: The parameters needed to send the request. - :return: None - """ - context.server.send_notification_message(cls.method_name, params.to_dict()) diff --git a/slither_lsp/lsp/requests/workspace/__init__.py b/slither_lsp/lsp/requests/workspace/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/requests/workspace/get_workspace_folders.py b/slither_lsp/lsp/requests/workspace/get_workspace_folders.py deleted file mode 100644 index 274b608..0000000 --- a/slither_lsp/lsp/requests/workspace/get_workspace_folders.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import List, Optional - -from slither_lsp.lsp.requests.base_request import BaseRequest -from slither_lsp.lsp.types.errors import CapabilitiesNotSupportedError -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.basic_structures import WorkspaceFolder - - -class GetWorkspaceFoldersRequest(BaseRequest): - """ - Request which obtains an array of workspace folders. - """ - - method_name = "workspace/workspaceFolders" - - @classmethod - def _check_capabilities(cls, context: ServerContext) -> None: - """ - Checks if the client has capabilities for this message. Throws a CapabilitiesNotSupportedError if it does not. - :param context: The server context which tracks state for the server. - :return: None - """ - if not (context.client_capabilities.workspace and context.client_capabilities.workspace.workspace_folders): - raise CapabilitiesNotSupportedError(cls) - - @classmethod - def send(cls, context: ServerContext) -> Optional[List[WorkspaceFolder]]: - """ - Sends a 'workspace/workspaceFolders' request to the client to obtain workspace folders. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#window_showMessage - :param context: The server context which determines the server to use to send the message. - :return: None - """ - # Check relevant capabilities - cls._check_capabilities(context) - - # Invoke the operation otherwise. - workspace_folders = context.server.send_request_message( - cls.method_name, - None - ) - - # If our workspace is None, we return None. - if workspace_folders is None: - return None - - # Parse our data - workspace_folders = [WorkspaceFolder.from_dict(folder) for folder in workspace_folders] - return workspace_folders diff --git a/slither_lsp/lsp/servers/__init__.py b/slither_lsp/lsp/servers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/servers/base_server.py b/slither_lsp/lsp/servers/base_server.py deleted file mode 100644 index 26c4572..0000000 --- a/slither_lsp/lsp/servers/base_server.py +++ /dev/null @@ -1,293 +0,0 @@ -import inspect -import traceback -from threading import Lock -from time import sleep -from typing import Any, Dict, IO, Optional, Type, Union - -from slither_lsp.lsp.request_handlers import registered_handlers -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.request_handlers.lifecycle.exit import ExitHandler -from slither_lsp.lsp.state.server_config import ServerConfig -from slither_lsp.lsp.io.event_emitter import AsyncEventEmitter -from slither_lsp.lsp.types.errors import LSPError, LSPErrorCode -from slither_lsp.lsp.io.jsonrpc_io import JsonRpcIo -from slither_lsp.lsp.state.server_context import ServerContext - -# Obtain all core request handlers for the LSP so we have a request method name -> request handler lookup -_CORE_REQUEST_HANDLERS: dict = { - ch.method_name: ch - for ch in [getattr(registered_handlers, name) for name in dir(registered_handlers)] - if inspect.isclass(ch) and ch != BaseRequestHandler and issubclass(ch, BaseRequestHandler) -} - -_HANDLER_POLLING_INTERVAL: float = 0.1 -_RESPONSE_POLLING_INTERVAL: float = 0.1 - - -class BaseServer: - """ - TODO: - """ - def __init__(self, server_config: ServerConfig): - self.running: bool = False - self.config = server_config - self.context: Optional[ServerContext] = None - self.io: Optional[JsonRpcIo] = None - self._pending_response_queue: Dict[Union[int, str], Optional[Any]] = {} - self._current_server_request_id = 0 - self._request_lock = Lock() - - # Create our main event emitter - self.event_emitter = AsyncEventEmitter() - - # Create our request handler lookup from our core request handler lookup and add additional handlers provided - # through the server configuration. - self._request_handlers = _CORE_REQUEST_HANDLERS.copy() - if self.config.additional_request_handlers is not None: - for additional_handler in self.config.additional_request_handlers: - if not issubclass(additional_handler, BaseRequestHandler): - raise ValueError( - f"Server configuration provided an additional request handler which was not the correct type." - ) - self._request_handlers[additional_handler.method_name] = additional_handler - - def _main_loop(self, read_file_handle: IO, write_file_handle: IO): - """ - The main entry point for the server, which begins accepting and processing request_handlers - on the given IO. - :return: None - """ - # Set our running state to True - self.running = True - - # Create a fresh copy of our initializing server capabilities if we have any. - server_capabilities = None - if self.config.initial_server_capabilities is not None: - server_capabilities = self.config.initial_server_capabilities.clone() - - # Reset server state and set our IO - self.context = ServerContext(self, server_capabilities=server_capabilities) - self.io = JsonRpcIo(read_file_handle, write_file_handle) - - # Continuously process messages. - while True: - try: - # Read a message, if there is none available, loop and wait for another. - result = self.io.read() - if result is None: - sleep(_HANDLER_POLLING_INTERVAL) - continue - - # Process the underlying message - (headers, message) = result - self._handle_message(message) - except ConnectionResetError as e: - # If the connection was reset, we exit the LSP using the exit handler - # (so a clean exit is invoked). - ExitHandler.process(self.context, None) - - def _handle_message(self, message: Any) -> None: - """ - The main dispatcher for a received message. It handles a request, notification, or response for a previously - made request. - :param message: The deserialized Language Server Protocol message received over JSON-RPC. - :return: None - """ - - # Verify the top level is a dictionary - if not isinstance(message, dict): - # This should be a dictionary at the top level, but we'll ignore requests that are - # malformed this bad. - return - - # If there's a method field, its a request or notification. If there isn't, it's a response - method_name = message.get('method') - if method_name is not None: - self._handle_request_or_notification(message) - else: - self._handle_response(message) - - def _handle_request_or_notification(self, message: Any) -> None: - """ - The dispatcher for a received request or notification. It determines which request handler to call and unpacks - arguments. - :param message: The deserialized Language Server Protocol message received over JSON-RPC. - :return: None - """ - # Fetch basic parameters - message_id = message.get('id') - method_name = message.get('method') - - # This should be a request or notification. - try: - # If the method name isn't a string, throw an invalid request error. - if not isinstance(method_name, str): - raise LSPError( - LSPErrorCode.InvalidRequest, - "'method' field should be a string type.", - None - ) - - # If this is a request and we're shutdown, return an error. - if message_id is None and self.context.shutdown: - raise LSPError( - LSPErrorCode.InvalidRequest, - "Cannot process additional requests once a shutdown request has been made.", - None - ) - - # Fetch the relevant request handler. If we don't have one, raise an error. - command_handler: Optional[Type[BaseRequestHandler]] = self._request_handlers.get(method_name) - if command_handler is None: - raise LSPError( - LSPErrorCode.MethodNotFound, - f"A request handler does not exist for method '{method_name}'", - None, - ) - - # Execute the relevant request handler and get the result. - try: - result = command_handler.process(self.context, message.get('params')) - except LSPError as err: - # If it's an LSPError, we simply re-raise it without wrapping it. - raise err - except Exception as err: - # Wrap any other exception in an LSPError exception and raise it - traceback_str = traceback.format_exc() - raise LSPError( - LSPErrorCode.InternalError, - f"An unhandled exception occurred:\r\n{traceback_str}", - traceback_str - ) from err - - # If we have a message id, it is a request, so we send back a response. - # Otherwise it's a notification and we don't do anything. - if message_id is not None: - self._send_response_message(message_id, result) - - except LSPError as lsp_error: - # If an LSP error occurred, we send it over the wire. - self._send_response_error( - message_id, - lsp_error.error_code, - lsp_error.error_message, - lsp_error.error_data - ) - - def _handle_response(self, message: Any) -> None: - """ - Handles a response from the client for a previously sent request. - :param message: The deserialized Language Server Protocol message received over JSON-RPC. - :return: None - """ - # Fetch basic parameters - message_id = message.get('id') - - # Ignore responses without an id or callback functions. - if message_id is None or (not isinstance(message_id, str) and not isinstance(message_id, int)): - return - - # Determine if this is an error or success result. - error_info = message.get('error') - if error_info is not None: - # We had an error result, unpack our error fields. - # TODO: If the error is malformed, we should introduce window here later. - # For now we ignore. - if not isinstance(error_info, dict): - return - - error_code: Optional[int] = error_info.get('code') - error_message: Optional[str] = error_info.get('message') - error_data: Any = error_info.get('data') - - # TODO: If the error is malformed, we should introduce window here later. - # For now we ignore. - self._pending_response_queue[message_id] = LSPError(LSPErrorCode(error_code), error_message, error_data) - else: - # We had a successful result. - self._pending_response_queue[message_id] = message.get('result') - - def send_request_message(self, method_name: str, params: Any) -> Any: - """ - Sends a request to the client, providing callback options in the event of success/error. - :param method_name: The name of the method to invoke on the client. - :param params: The parameters to send with the request. - :return: None - """ - # Lock to avoid request id collisions - with self._request_lock: - # Generate a request id - request_id: Union[str, int] = f"slither-lsp-{self._current_server_request_id}" - - # Increment the request id - self._current_server_request_id += 1 - - # Send the request to the client - self.io.write({ - 'jsonrpc': '2.0', - 'id': request_id, - 'method': method_name, - 'params': params - }) - - # Wait for a response - while request_id not in self._pending_response_queue: - sleep(_RESPONSE_POLLING_INTERVAL) - - # Obtain the response from the queue. If it was an LSP error, raise it. - response = self._pending_response_queue.pop(request_id) - if isinstance(response, LSPError): - raise LSPError( - LSPErrorCode.InternalError, - f"Request '{method_name}' failed:\r\n{response.error_message}", - None - ) - - # Return the response data - return response - - def _send_response_message(self, message_id: Union[int, str, None], result: Any) -> None: - """ - Sends a response back to the client in the event of a successful operation. - :param message_id: The message id to respond to with this message. - :param result: The resulting data to respond with in response to a previous request which used message_id. - :return: None - """ - self.io.write({ - 'jsonrpc': '2.0', - 'id': message_id, - 'result': result - }) - - def _send_response_error(self, message_id: Union[int, str, None], error_code: LSPErrorCode, error_message: str, - error_data: Any) -> None: - """ - Sends an error response back to the client. - :param message_id: The message id to respond to with this error. - :param error_code: The error code to send across the wire. - :param error_message: A short description of the error to be supplied to the client. - :param error_data: Optional additional data which can be included with the error. - :return: None - """ - self.io.write({ - 'jsonrpc': '2.0', - 'id': message_id, - 'error': { - 'code': int(error_code), - 'message': error_message, - 'data': error_data - } - }) - - def send_notification_message(self, method_name: str, params: Any) -> None: - """ - Sends a notification to the client which targeting a specific method. - :param method_name: The name of the method to invoke with this notification. - :param params: The additional data provided to the underlying method. - :return: None - """ - self.io.write({ - 'jsonrpc': '2.0', - 'method': method_name, - 'params': params - }) diff --git a/slither_lsp/lsp/servers/console_server.py b/slither_lsp/lsp/servers/console_server.py deleted file mode 100644 index 781674b..0000000 --- a/slither_lsp/lsp/servers/console_server.py +++ /dev/null @@ -1,68 +0,0 @@ -import io -import sys -from typing import List, Optional, TextIO - -from slither_lsp.lsp.servers.base_server import BaseServer -from slither_lsp.lsp.state.server_config import ServerConfig - - -class NullStringIO(io.StringIO): - """ - I/O implementation which captures output, and optionally mirrors it to the original I/O stream it replaces. - """ - - def write(self, s): - """ - The write operation for this StringIO does nothing. - :param s: The provided string which should be written to IO (but is discarded). - :return: None - """ - pass - - def writelines(self, __lines: List[str]) -> None: - """ - The writelines operation for this IO is overridden to do nothing. - :param __lines: A list of lines to be discarded. - :return: None - """ - pass - - -class ConsoleServer(BaseServer): - """ - Provides a console (stdin/stdout) interface for JSON-RPC - """ - def __init__(self, server_config: ServerConfig): - self._actual_stdin: Optional[TextIO] = None - self._actual_stdout: Optional[TextIO] = None - self._actual_stderr: Optional[TextIO] = None - super().__init__(server_config=server_config) - - def start(self): - """ - Starts the server to begin accepting and processing request_handlers on the given IO. - :return: None - """ - # Fetch our stdio handles which we will use to communicate, then - self._actual_stdin = sys.stdin - self._actual_stdout = sys.stdout - self._actual_stderr = sys.stderr - - # Now that we have backed up the stdio handles, globally redirect stdout/stderr to a null - # stream so that all other code which could print does not disturb server communications. - sys.stdin = NullStringIO() - sys.stdout = NullStringIO() - sys.stderr = NullStringIO() - - # Start our server using stdio in binary mode for the provided IO handles. - self._main_loop(self._actual_stdin.buffer, self._actual_stdout.buffer) - - def stop(self): - """ - Stops the server from processing requests and restores the previously suppressed stdio handles. - :return: None - """ - # If execution has ceased, restore the stdio file handles so that printing can resume as usual. - sys.stdin = self._actual_stdin - sys.stdout = self._actual_stdout - sys.stderr = self._actual_stderr diff --git a/slither_lsp/lsp/servers/network_server.py b/slither_lsp/lsp/servers/network_server.py deleted file mode 100644 index 5fcf156..0000000 --- a/slither_lsp/lsp/servers/network_server.py +++ /dev/null @@ -1,60 +0,0 @@ -import socket -from threading import Thread -from typing import Optional - -from slither_lsp.lsp.servers.base_server import BaseServer -from slither_lsp.lsp.state.server_config import ServerConfig - - -class NetworkServer(BaseServer): - """ - Provides a TCP network socket interface for JSON-RPC - """ - def __init__(self, port: int, server_config: ServerConfig): - # Set our port and initialize our socket - self.port = port - self._server_socket: Optional[socket] = None - self._thread: Optional[Thread] = None - super().__init__(server_config=server_config) - - def start(self): - """ - Starts the server to begin accepting and processing request_handlers on the given IO. - :return: None - """ - # Create a socket to accept our connections - self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # Bind our socket - # TODO: For now we only allow one connection, determine if we should allow multiple in the future. - self._server_socket.bind(('127.0.0.1', self.port)) - self._server_socket.listen(1) - - # Accept connections on another thread. - self._thread = Thread( - target=self._accept_connections, - args=() - ) - self._thread.start() - - def _accept_connections(self): - """ - A blocking function which accepts incoming connections and begins processing requests. - :return: None - """ - # Accept connections and process with the underlying IO handlers. - while True: - # Accept a new connection, create a file handle which we will use to process our main loop - connection_socket, address = self._server_socket.accept() - connection_file_handle = connection_socket.makefile(mode='rwb', encoding='utf-8') - - # Enter the main loop, this will reset state, so each connection will reset state. - self._main_loop(connection_file_handle, connection_file_handle) - - def stop(self): - """ - Stops the server from processing requests and tears down the underlying socket. - :return: None - """ - # TODO: Kill thread, etc. - self._server_socket.close() diff --git a/slither_lsp/lsp/state/__init__.py b/slither_lsp/lsp/state/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/state/server_config.py b/slither_lsp/lsp/state/server_config.py deleted file mode 100644 index c5b4802..0000000 --- a/slither_lsp/lsp/state/server_config.py +++ /dev/null @@ -1,22 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional, List, Type - -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.state.server_hooks import ServerHooks -from slither_lsp.lsp.types.capabilities import ServerCapabilities - - -@dataclass -class ServerConfig: - """ - Represents of set of configuration variables which can be passed to a server to initialize it with. - """ - # The initial capabilities this server will broadcast to clients. This can be changed with dynamic registration - # requests after a connection is established. - initial_server_capabilities: ServerCapabilities - - # Hooks which can be used to fulfill language feature requests. - hooks: Optional[ServerHooks] = None - - # List of additional request handlers to register with the server - additional_request_handlers: List[Type[BaseRequestHandler]] = field(default_factory=list) diff --git a/slither_lsp/lsp/state/server_context.py b/slither_lsp/lsp/state/server_context.py deleted file mode 100644 index 1141376..0000000 --- a/slither_lsp/lsp/state/server_context.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Optional - -from pkg_resources import require -from slither_lsp.lsp.types.basic_structures import ClientServerInfo, TraceValue -from slither_lsp.lsp.types.capabilities import ClientCapabilities, ServerCapabilities - - -class ServerContext: - def __init__(self, server, server_capabilities=None): - # Import some items late here - import slither_lsp.lsp.servers.base_server as base_server - - # Create our basic LSP state variables - self.server_initialized: bool = False - self.client_initialized: bool = False - self.shutdown: bool = False - self.trace: TraceValue = TraceValue.OFF - self.server: base_server.BaseServer = server - self.client_info: Optional[ClientServerInfo] = None - self.client_capabilities: ClientCapabilities = ClientCapabilities() - self.server_capabilities: ServerCapabilities = server_capabilities or ServerCapabilities() - - @property - def server_hooks(self): - """ - Represents a set of hooks which can be used to fulfill requests. - :return: Returns the server hook object used to fulfill requests. - """ - return self.server.config.hooks - - @property - def event_emitter(self): - """ - Represents the main event emitter used by this server. This simply forwards to server.event_emitter. - :return: Returns the main event emitter used by this server. - """ - return self.server.event_emitter - - @property - def server_info(self) -> ClientServerInfo: - return ClientServerInfo( - name='Slither Language Server', - version=require("slither-lsp")[0].version - ) diff --git a/slither_lsp/lsp/state/server_hooks.py b/slither_lsp/lsp/state/server_hooks.py deleted file mode 100644 index a61348e..0000000 --- a/slither_lsp/lsp/state/server_hooks.py +++ /dev/null @@ -1,78 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Union, List, Optional - -from slither_lsp.lsp.state.server_context import ServerContext -from slither_lsp.lsp.types.basic_structures import Location, LocationLink -from slither_lsp.lsp.types.params import DeclarationParams, DefinitionParams, TypeDefinitionParams, \ - ImplementationParams, HoverParams, Hover - - -class ServerHooks(ABC): - """ - Defines a set of hooks which the server can use to fulfill request responses. - """ - - @abstractmethod - def hover(self, context: ServerContext, params: HoverParams) -> Optional[Hover]: - """ - Resolves a resolves hover information at a given text document position. - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Hover | None - """ - return None - - @abstractmethod - def goto_declaration(self, context: ServerContext, params: DeclarationParams) \ - -> Union[Location, List[Location], List[LocationLink], None]: - """ - Resolves a declaration location of a symbol at a given text document position. - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location | Location[] | LocationLink[] | None - """ - return None - - @abstractmethod - def goto_definition(self, context: ServerContext, params: DefinitionParams) \ - -> Union[Location, List[Location], List[LocationLink], None]: - """ - Resolves a definition location of a symbol at a given text document position. - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location | Location[] | LocationLink[] | None - """ - return None - - @abstractmethod - def goto_type_definition(self, context: ServerContext, params: TypeDefinitionParams) \ - -> Union[Location, List[Location], List[LocationLink], None]: - """ - Resolves a type definition location of a symbol at a given text document position. - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location | Location[] | LocationLink[] | None - """ - return None - - @abstractmethod - def goto_implementation(self, context: ServerContext, params: ImplementationParams) \ - -> Union[Location, List[Location], List[LocationLink], None]: - """ - Resolves a implementation location of a symbol at a given text document position. - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location | Location[] | LocationLink[] | None - """ - return None - - @abstractmethod - def find_references(self, context: ServerContext, params: ImplementationParams) \ - -> Union[List[Location], None]: - """ - Resolves project-wide references for the symbol denoted by the given text document position. - :param context: The server context which determines the server to use to send the message. - :param params: The parameters object provided with this message. - :return: Location[] | None - """ - return None diff --git a/slither_lsp/lsp/types/__init__.py b/slither_lsp/lsp/types/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/slither_lsp/lsp/types/base_serializable_structure.py b/slither_lsp/lsp/types/base_serializable_structure.py deleted file mode 100644 index 9a51170..0000000 --- a/slither_lsp/lsp/types/base_serializable_structure.py +++ /dev/null @@ -1,421 +0,0 @@ -import re -import inspect -from abc import ABC, abstractmethod -from dataclasses import fields, dataclass, is_dataclass -from enum import Enum, IntEnum, IntFlag -from typing import Any, Optional, Type, Set, List, Union, get_args, get_origin, Tuple - - -def _to_camel_case(s): - """ - Converts a snake case string into a camel case string. - :param s: The snake string to convert into camel case. - :return: Returns the resulting camel case string. - """ - # Split string on underscore. Output first part but capitalize the first letter of the other parts and join them. - parts = s.split('_') - return parts[0] + ''.join(x.title() for x in parts[1:]) - - -def _get_potential_types(obj_type: Type) -> Tuple[Optional[Type], List[Type]]: - origin_type = get_origin(obj_type) - args = get_args(obj_type) - return origin_type, args - - -@dataclass -class _EmptyDataClass: - """ - This class serves as a class with an unconfigured variable which is used to resolve the _MISSING type - used to signal 'default' and 'default_factory' being unset. - """ - plain_value: Any - - -# Obtain the type used for empty 'default' and 'default_factory' parameters. -_EMPTY_DEFAULT_TYPE = type(fields(_EmptyDataClass)[0].default) - - -def _get_default_value(field): - """ - Obtains a default value for a given field. - :param field: The field to obtain the default value for. - :return: Returns a default value for a given field. - """ - if not isinstance(field.default, _EMPTY_DEFAULT_TYPE): - return field.default - elif not isinstance(field.default_factory, _EMPTY_DEFAULT_TYPE): - return field.default_factory() - return None - - -def serialization_metadata(name_override: str = None, include_none: Optional[bool] = None, - enforce_as_constant: Optional[bool] = None) -> dict: - """ - Creates metadata for python dataclasses, to be used with SerializableStructure to convey additional serialization - information. - :param name_override: If not None, denotes an override for the key name when serializing a dataclass field. - :param include_none: If not None, denotes whether None/null keys should be included. - :param enforce_as_constant: If not None, treats the default value as a constant which needs to be enforced strictly. - :return: Returns a dictionary containing the metadata keys to be expected - """ - metadata = {} - if name_override is not None: - metadata['name'] = name_override - if include_none is not None: - metadata['include_none'] = include_none - if enforce_as_constant is not None: - metadata['enforce_as_constant'] = enforce_as_constant - return metadata - - -@dataclass -class SerializableStructure(ABC): - """ - Represents a structure which is serializable to/deserializable from generic LSP structures. - """ - - def __init__(self, **kwargs): - """ - Empty constructor, overriden by the @dataclass property. This simply satisfies the linter when instantiating - with arbitary arguments. - :param kwargs: Arbitrary argument array. - """ - pass - - def clone(self) -> 'SerializableStructure': - """ - Creates a clone of the serializable object. - :return: Returns a clone of this structured object. - """ - return self.from_dict(self.to_dict()) - - @classmethod - def _serialize_field(cls, field_value: Any, field_type: Type) -> Any: - # Obtain type information. - origin_type, origin_type_args = _get_potential_types(field_type) - - # Obtain a list of types that satisfy this field. If this is a union, it's arguments are our satisfying types. - satisfying_types: List[Type] = [] - if origin_type is Union: - satisfying_types.extend(origin_type_args) - else: - satisfying_types.append(field_type) - - # If the value is None and we allow None types, return it. Otherwise throw an error. - if field_value is None: - if type(None) in satisfying_types or Any in satisfying_types: - return None - else: - raise ValueError("Error deserializing field. Expected a non-None type, but got None") - - # It can't be a none type now, so remove it from our list of potential types - if type(None) in satisfying_types: - satisfying_types.remove(type(None)) - - # Loop through all our satisfying types. - for satisfying_type in satisfying_types: - - satisfying_type_is_class = inspect.isclass(satisfying_type) - if satisfying_type_is_class: - # Handle ints - if satisfying_type is int and isinstance(field_value, int) or \ - satisfying_type is str and isinstance(field_value, str) or \ - satisfying_type is float and isinstance(field_value, float) or \ - satisfying_type is bool and isinstance(field_value, bool): - return field_value - - # Handle enums - if (satisfying_type_is_class and issubclass(satisfying_type, IntEnum) and - isinstance(field_value, satisfying_type)) or \ - (satisfying_type is Any and isinstance(field_value, IntEnum)): - field_value: IntEnum - return field_value.value - if (satisfying_type_is_class and issubclass(satisfying_type, IntFlag) and - isinstance(field_value, satisfying_type)) or \ - (satisfying_type is Any and isinstance(field_value, IntFlag)): - field_value: IntFlag - return field_value.value - if (satisfying_type_is_class and issubclass(satisfying_type, Enum) and - isinstance(field_value, satisfying_type)) or \ - (satisfying_type is Any and isinstance(field_value, Enum)): - field_value: Enum - return field_value.value - - # If our current satisfying type is a serializable structure and we have a dict, simply serialize it - if (satisfying_type_is_class and issubclass(satisfying_type, SerializableStructure) and - isinstance(field_value, satisfying_type)) or \ - (satisfying_type is Any and isinstance(field_value, SerializableStructure)): - return field_value.to_dict() - - # Handle lists if our field value is a list - if isinstance(field_value, list): - try: - # Obtain information about the satisfying type - if inspect.isclass(satisfying_type) and satisfying_type is list: - potential_list_origin_type = list - potential_list_element_types = [] - else: - potential_list_origin_type, potential_list_element_types = _get_potential_types(satisfying_type) - - if potential_list_origin_type is not None and issubclass(potential_list_origin_type, list): - # Obtain our key/value types. If we don't have one, we use 'Any' by default - element_type = Any - if len(potential_list_element_types) > 0: - element_type = potential_list_element_types[0] - - # Deserialize the list accordingly. - serialized_list = [ - cls._serialize_field(element, element_type) - for element in field_value - ] - return serialized_list - except ValueError: - pass - - # Handle dictionaries if our field value is a dictionary - if isinstance(field_value, dict): - try: - # Obtain information about the satisfying type - if inspect.isclass(satisfying_type) and satisfying_type is dict: - potential_list_origin_type = dict - potential_list_element_types = [] - else: - potential_list_origin_type, potential_list_element_types = _get_potential_types(satisfying_type) - - if potential_list_origin_type is not None and issubclass(potential_list_origin_type, dict): - # Obtain our key/value types. If we don't have one, we use 'Any' by default - dict_key_type = Any - dict_value_type = Any - if len(potential_list_element_types) > 1: - dict_key_type = potential_list_element_types[0] - dict_value_type = potential_list_element_types[1] - - # Deserialize the dictionary accordingly. - serialized_dict = {} - for dict_key, dict_value in field_value.items(): - serialized_key = cls._serialize_field(dict_key, dict_key_type) - serialized_value = cls._serialize_field(dict_value, dict_value_type) - serialized_dict[serialized_key] = serialized_value - return serialized_dict - except ValueError: - pass - - # If our satisfying type is any, we can return the data as is. - if satisfying_type is Any: - return field_value - - # The value is not none. If it is a type in the field types, return it. - raise ValueError() - - @classmethod - def _deserialize_field(cls, serialized_value: Any, field_type: Type) -> Any: - # Obtain type information. - origin_type, origin_type_args = _get_potential_types(field_type) - - # Obtain a list of types that satisfy this field. If this is a union, it's arguments are our satisfying types. - satisfying_types: List[Type] = [] - if origin_type is Union: - satisfying_types.extend(origin_type_args) - else: - satisfying_types.append(field_type) - - # If the value is None and we allow None types, return it. Otherwise throw an error. - if serialized_value is None: - if type(None) in satisfying_types or Any in satisfying_types: - return None - else: - raise ValueError("Error deserializing field. Expected a non-None type, but got None") - - # It can't be a none type now, so remove it from our list of potential types - if type(None) in satisfying_types: - satisfying_types.remove(type(None)) - - # Loop through all our satisfying types. - for satisfying_type in satisfying_types: - - # If our satisfying type is any, we can return the data as is. - if satisfying_type is Any: - return serialized_value - - # Some satisfying types will be classes, these are typical object types. - # Otherwise, they will be typing.* objects that we resolve in special cases below. - if inspect.isclass(satisfying_type): - # Handle ints - if satisfying_type is int and isinstance(serialized_value, int) or \ - satisfying_type is str and isinstance(serialized_value, str) or \ - satisfying_type is float and isinstance(serialized_value, float) or \ - satisfying_type is bool and isinstance(serialized_value, bool): - return serialized_value - - # Handle enums - if issubclass(satisfying_type, IntEnum) and isinstance(serialized_value, int): - return satisfying_type(serialized_value) - if issubclass(satisfying_type, IntFlag) and isinstance(serialized_value, int): - return satisfying_type(serialized_value) - if issubclass(satisfying_type, Enum) and isinstance(serialized_value, str): - return satisfying_type(serialized_value) - - # Handle flags - - - # If our current satisfying type is a serializable structure and we have a dict, try to deserialize - # with it. - if issubclass(satisfying_type, SerializableStructure) and isinstance(serialized_value, dict): - try: - deserialized_value = satisfying_type.from_dict(serialized_value) - return deserialized_value - except ValueError: - pass - - # Handle lists if our serialized value is a list - if isinstance(serialized_value, list): - try: - # Obtain information about the satisfying type - if inspect.isclass(satisfying_type) and satisfying_type is list: - potential_list_origin_type = list - potential_list_element_types = [] - else: - potential_list_origin_type, potential_list_element_types = _get_potential_types(satisfying_type) - - if potential_list_origin_type is not None and issubclass(potential_list_origin_type, list): - # Obtain our element type. If we don't have one, we use 'Any' by default - element_type = Any - if len(potential_list_element_types) > 0: - element_type = potential_list_element_types[0] - - # Deserialize the list accordingly. - deserialized_list = [ - cls._deserialize_field(serialized_element, element_type) - for serialized_element in serialized_value - ] - return deserialized_list - except ValueError: - pass - - # Handle dictionaries if our serialized value is a dictionary - if isinstance(serialized_value, dict): - try: - # Obtain information about the satisfying type - if inspect.isclass(satisfying_type) and satisfying_type is dict: - potential_list_origin_type = dict - potential_list_element_types = [] - else: - potential_list_origin_type, potential_list_element_types = _get_potential_types(satisfying_type) - - if potential_list_origin_type is not None and issubclass(potential_list_origin_type, dict): - # Obtain our element type. If we don't have one, we use 'Any' by default - dict_key_type = Any - dict_value_type = Any - if len(potential_list_element_types) > 1: - dict_key_type = potential_list_element_types[0] - dict_value_type = potential_list_element_types[1] - - # Deserialize the dictionary accordingly. - deserialized_dict = {} - for serialize_dict_key, serialize_dict_value in serialized_value.items(): - deserialized_key = cls._deserialize_field(serialize_dict_key, dict_key_type) - deserialized_value = cls._deserialize_field(serialize_dict_value, dict_value_type) - deserialized_dict[deserialized_key] = deserialized_value - return deserialized_dict - except ValueError: - pass - - # The value is not none. If it is a type in the field types, return it. - raise ValueError() - - @classmethod - def from_dict(cls, obj: dict) -> 'SerializableStructure': - """ - Parses the provided object into an instance of the class. - :param obj: The dictionary object to parse this structure from. - :return: Returns an instance of the class. - """ - # Create our initialization arguments. - init_args: dict = {} - - # Obtain the fields for this item - fields_list = fields(cls) - - # Loop for each field to populate it - for field in fields_list: - # Obtain field information - serialized_field_name: str = field.name - field_metadata: dict = field.metadata - - # If we have a name override, we use that name - name_override = field_metadata.get('name') - if name_override is not None: - serialized_field_name = name_override - else: - # If we don't have an override, we convert to camel case - serialized_field_name = _to_camel_case(serialized_field_name) - - # Obtain our serialized value - serialized_field_value = obj.get(serialized_field_name) - - # If this field existed in our object, deserialize it and set its value. - if serialized_field_name in obj: - # Deserialize the field value - field_value = cls._deserialize_field(serialized_field_value, field.type) - - # If we are enforcing a constant, we raise an error if it does not match the default value. - enforce_as_constant = field_metadata.get('enforce_as_constant') - if enforce_as_constant: - default_value = _get_default_value(field) - if field_value != default_value: - raise ValueError( - f"Field {field.name} could not be deserialized because metadata defined it as a constant " - f"and the provided value did not equal the default value." - ) - - init_args[field.name] = field_value - else: - # Otherwise set the default value if we were provided one, otherwise we use None as a default. - default_value = _get_default_value(field) - init_args[field.name] = default_value - - # Use the parsed arguments to instantiate a copy of this class - return cls(**init_args) - - def to_dict(self) -> dict: - """ - Dumps an instance of this class to a dictionary object. It reads all relevant properties for this immediate - class and classes it had inherited from. - :return: Returns a dictionary object that represents an instance of this data. - """ - # Create our resulting dictionary - result = {} - - # Obtain the fields for this item - fields_list: tuple = fields(self) - - # Loop for each field to populate it - for field in fields_list: - # Obtain field information - serialized_field_name: str = field.name - field_value: Any = getattr(self, field.name) - field_metadata: dict = field.metadata - - # If we have a name override, we use that name - name_override = field_metadata.get('name') - if name_override is not None: - serialized_field_name = name_override - else: - # If we don't have an override, we convert to camel case - serialized_field_name = _to_camel_case(serialized_field_name) - - # If we are enforcing the default as a constant, we overwrite the field value with the default. - enforce_as_constant = field_metadata.get('enforce_as_constant') - if enforce_as_constant: - field_value = _get_default_value(field) - - # Serialize this field - serialized_field_value = self._serialize_field(field_value, field.type) - - # Determine if we should set the result. By default None is excluded, unless an override is provided - include_none = field_metadata.get('include_none') - if serialized_field_value is not None or include_none: - result[serialized_field_name] = serialized_field_value - - return result diff --git a/slither_lsp/lsp/types/basic_structures.py b/slither_lsp/lsp/types/basic_structures.py deleted file mode 100644 index 621bddb..0000000 --- a/slither_lsp/lsp/types/basic_structures.py +++ /dev/null @@ -1,625 +0,0 @@ -from dataclasses import dataclass, field -from enum import IntEnum, Enum -from typing import Optional, Any, Union, List, Dict - -# These structures ideally would just be dataclass objects, so we could cast dictionaries to dataclasses. -# However, dataclasses cannot initialize with unexpected parameters, and we can't assume the Language Server -# Protocol won't change and add more keys. So we add our own serializing/deserializing methods on top of -# this while still reaping benefits of auto-constructor generation, parameter validation, etc from dataclass. -# See more at the link below: -# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#basic-json-structures - -# Text documents have a defined EOL. -# https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocuments -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure, serialization_metadata - -EOL = ['\n', '\r\n', '\r'] - - -@dataclass -class ClientServerInfo(SerializableStructure): - """ - Data structure which describes a client/server by name and version. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#initialize - """ - - # The name of the client/server as defined by itself. - name: str - - # The client/server's version as defined by itself. - version: Optional[str] = None - - -class MessageType(IntEnum): - """ - Defines the severity level of a message to be shown/logged. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#messageType - """ - ERROR = 1 - WARNING = 2 - INFO = 3 - LOG = 4 - - -@dataclass -class WorkspaceFolder(SerializableStructure): - """ - Data structure which describes a workspace folder by name and location (uri). - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceFolder - """ - # The associated URI for this workspace folder. - uri: str - - # The name of the workspace folder. Used to refer to this - # workspace folder in the user interface. - name: Optional[str] = None - - -@dataclass -class Position(SerializableStructure): - """ - Data structure which represents a position within a text file by line number and character index (column). - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#position - """ - # Line position in a document (zero-based). - line: int - - # Character offset on a line in a document (zero-based). Assuming that - # the line is represented as a string, the `character` value represents - # the gap between the `character` and `character + 1`. - # - # If the character value is greater than the line length it defaults back - # to the line length. - character: int - - -@dataclass -class Range(SerializableStructure): - """ - Data structure which represents a position range in a text file. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#range - """ - # The range's start position. - start: Position - - # The range's end position. - end: Position - - -@dataclass -class Location(SerializableStructure): - """ - Data structure which represents a text file location (file uri and position range). - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#location - """ - uri: str - range: Range - - -@dataclass -class LocationLink(SerializableStructure): - """ - Data structure which represents a link between a source and target destination. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#locationLink - """ - # The target resource identifier of this link. - target_uri: str - - # The full target range of this link. If the target for example is a symbol - # then target range is the range enclosing this symbol not including - # leading/trailing whitespace but everything else like comments. This - # information is typically used to highlight the range in the editor. - target_range: Range - - # The range that should be selected and revealed when this link is being - # followed, e.g the name of a function. Must be contained by the the - # `targetRange`. See also `DocumentSymbol#range` - target_selection_range: Range - - # Span of the origin of the link. - # Used as the underlined span for mouse interaction. Defaults to the word - # range at the mouse position. - origin_selection_range: Optional[Range] = None - - -class DiagnosticSeverity(IntEnum): - """ - Defines the severity level of a diagnostic (compiler error, warning, etc). - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#diagnosticSeverity - """ - ERROR = 1 - WARNING = 2 - INFORMATION = 3 - HINT = 4 - - -class DiagnosticTag(IntEnum): - """ - Diagnostic tags describe code. Tags include 'unnecessary', 'deprecated', etc. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#diagnosticTag - """ - # Unused or unnecessary code. - # - # Clients are allowed to render diagnostics with this tag faded out - # instead of having an error squiggle. - UNNECESSARY = 1 - - # Deprecated or obsolete code. - # - # Clients are allowed to rendered diagnostics with this tag strike through. - DEPRECATED = 2 - - -@dataclass -class DiagnosticRelatedInformation(SerializableStructure): - """ - Data structure which represents a related message and source code location for a diagnostic. - This should be used to point to code locations that cause or are related to a diagnostic, e.g when duplicating a - symbol in scope. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#diagnosticRelatedInformation - """ - location: Location - message: str - - -@dataclass -class CodeDescription(SerializableStructure): - """ - Data structure which represents a description for an error code. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#codeDescription - """ - href: str - - -@dataclass -class Diagnostic(SerializableStructure): - """ - Data structure which represents a diagnostic (compiler error, warning, etc). Diagnostic objects are only valid - in the scope of a resource. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#diagnostic - """ - # The range at which the message applies. - range: Range - - # The diagnostic's message. - message: str - - # The diagnostic's severity. Can be omitted. If omitted it is up to the - # client to interpret diagnostics as error, warning, info or hint. - severity: Optional[DiagnosticSeverity] = None - - # The diagnostic's code, which might appear in the user interface. - code: Union[int, str, None] = None - - # An optional property to describe the error code. - code_description: Optional[CodeDescription] = None - - # A human-readable string describing the source of this - # diagnostic, e.g. 'typescript' or 'super lint'. - source: Optional[str] = None - - # Additional metadata about the diagnostic. - tags: Optional[List[DiagnosticTag]] = None - - # An array of related diagnostic information, e.g. when symbol-names within - # a scope collide all definitions can be marked via this property. - related_information: Optional[List[DiagnosticRelatedInformation]] = None - - # A data entry field that is preserved between a - # `textDocument/publishDiagnostics` notification and - # `textDocument/codeAction` request. - data: Any = None - - -@dataclass -class Command(SerializableStructure): - """ - Data structure which represents a command which can be registered on the client side to be invoked on the server. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-current/#command - """ - # The title of the command, like `save`. - title: str - - # The identifier of the actual command handler - command: str - - # Arguments that the command handler should be invoked with - arguments: Optional[List[Any]] = None - - -@dataclass -class TextEdit(SerializableStructure): - """ - Data structure which represents a textual edit applicable to a text document. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textEdit - """ - # The range of the text document to be manipulated. To insert - # text into a document create a range where start === end. - range: Range - - # The string to be inserted. For delete operations use an - # empty string. - new_text: str - - -@dataclass -class ChangeAnnotation(SerializableStructure): - """ - Data structure which represents additional information regarding document changes. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#changeAnnotation - """ - # A human-readable string describing the actual change. The string - # is rendered prominent in the user interface. - label: str - - # A flag which indicates that user confirmation is needed - # before applying the change. - needs_confirmation: Optional[bool] = None - - # A human-readable string which is rendered less prominent in - # the user interface. - description: Optional[str] = None - - -@dataclass -class AnnotatedTextEdit(TextEdit): - """ - Data structure which represents a special text edit with an additional change annotation. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#annotatedTextEdit - """ - # The actual annotation identifier. - annotation_id: str - - -@dataclass -class TextDocumentIdentifier(SerializableStructure): - """ - Data structure which represents a text document identifier (uri). - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentIdentifier - """ - # The actual annotation identifier. - uri: str - - -@dataclass -class VersionedTextDocumentIdentifier(TextDocumentIdentifier): - """ - Data structure which represents an identifier to denote a specific version of a text document. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#versionedTextDocumentIdentifier - """ - - # The version number of this document. If an optional versioned text document - # identifier is sent from the server to the client and the file is not - # open in the editor (the server has not received an open notification - # before) the server can send `null` to indicate that the version is - # known and the content on disk is the master (as specified with document - # content ownership). - # - # The version number of a document will increase after each change, - # including undo/redo. The number doesn't need to be consecutive. - version: Optional[int] = field(default=None, metadata=serialization_metadata(include_none=True)) # int | null - - -@dataclass -class OptionalVersionedTextDocumentIdentifier(TextDocumentIdentifier): - """ - Data structure which represents an identifier which optionally denotes a specific version of a text document. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#optionalVersionedTextDocumentIdentifier - """ - - # The version number of this document. If an optional versioned text document - # identifier is sent from the server to the client and the file is not - # open in the editor (the server has not received an open notification - # before) the server can send `null` to indicate that the version is - # known and the content on disk is the master (as specified with document - # content ownership). - # - # The version number of a document will increase after each change, - # including undo/redo. The number doesn't need to be consecutive. - version: Optional[int] = field(default=None, metadata=serialization_metadata(include_none=True)) # int | null - - -@dataclass -class TextDocumentEdit(SerializableStructure): - """ - Data structure which represents describes textual changes on a single text document. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentEdit - """ - - # The text document to change. - text_document: OptionalVersionedTextDocumentIdentifier - - # The edits to be applied. - # @since 3.16.0 - support for AnnotatedTextEdit. This is guarded by the - # client capability `workspace.workspaceEdit.changeAnnotationSupport` - edits: List[Union[AnnotatedTextEdit, TextEdit]] - - -@dataclass -class CreateFileOptions(SerializableStructure): - """ - Data structure which represents options to create a file. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#createFileOptions - """ - # Overwrite existing file. Overwrite wins over `ignoreIfExists` - overwrite: Optional[bool] - - # Ignore if exists. - ignore_if_exists: Optional[bool] - - -@dataclass -class CreateFile(SerializableStructure): - """ - Data structure which represents a create file operation. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#createFile - """ - # The resource to create. - uri: str - - # Additional options - options: Optional[CreateFileOptions] - - # An optional annotation identifier describing the operation. - # @since 3.16.0 - annotation_id: Optional[str] - - # A create - kind: str = field(default='create', metadata=serialization_metadata(enforce_as_constant=True)) - - -@dataclass -class RenameFileOptions(SerializableStructure): - """ - Data structure which represents options to rename a file. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#renameFileOptions - """ - # Overwrite target if existing. Overwrite wins over `ignoreIfExists` - overwrite: Optional[bool] - - # Ignore if target exists. - ignore_if_exists: Optional[bool] - - -@dataclass -class RenameFile(SerializableStructure): - """ - Data structure which represents a rename file operation. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#renameFile - """ - # The old (existing) location. - old_uri: str - - # The new location. - new_uri: str - - # Rename options - options: Optional[RenameFileOptions] - - # An optional annotation identifier describing the operation. - # @since 3.16.0 - annotation_id: Optional[str] - - # A rename - kind: str = field(default='rename', metadata=serialization_metadata(enforce_as_constant=True)) - - -@dataclass -class DeleteFileOptions(SerializableStructure): - """ - Data structure which represents options to delete a file. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#deleteFileOptions - """ - # Delete the content recursively if a folder is denoted. - recursive: Optional[bool] - - # Ignore if target exists. - ignore_if_exists: Optional[bool] - - -@dataclass -class DeleteFile(SerializableStructure): - """ - Data structure which represents a delete file operation. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#deleteFile - """ - # The file to delete. - uri: str - - # Delete options. - options: Optional[DeleteFileOptions] - - # An optional annotation identifier describing the operation. - # @since 3.16.0 - annotation_id: Optional[str] - - # A delete - kind: str = field(default='delete', metadata=serialization_metadata(enforce_as_constant=True)) - - -@dataclass -class WorkspaceEdit(SerializableStructure): - """ - Data structure which represents changes to many resources managed in the workspace. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceEdit - """ - # Holds changes to existing resources. - changes: Optional[Dict[str, List[TextEdit]]] - - # Depending on the client capability - # `workspace.workspaceEdit.resourceOperations` document changes are either - # an array of `TextDocumentEdit`s to express changes to n different text - # documents where each text document edit addresses a specific version of - # a text document. Or it can contain above `TextDocumentEdit`s mixed with - # create, rename and delete file / folder operations. - # - # Whether a client supports versioned document edits is expressed via - # `workspace.workspaceEdit.documentChanges` client capability. - # - # If a client neither supports `documentChanges` nor - # `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s - # using the `changes` property are supported. - document_changes: Union[List[TextDocumentEdit], List[Union[TextDocumentEdit, CreateFile, RenameFile, DeleteFile]]] - - # A map of change annotations that can be referenced in - # `AnnotatedTextEdit`s or create, rename and delete file / folder - # operations. - # - # Whether clients honor this property depends on the client capability - # `workspace.changeAnnotationSupport`. - # - # @since 3.16.0 - change_annotations: Dict[List[str], ChangeAnnotation] # List[annotationId]: ChangeAnnotation - - -@dataclass -class TextDocumentItem(SerializableStructure): - """ - Data structure which represents an item to transfer a text document from the client to the server. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentItem - """ - # The text document's URI. - uri: str - - # The text document's language identifier. See language ids in the reference for this structure. - language_id: str - - # The version number of this document (it will increase after each change, including undo/redo). - version: int - - # The content of the opened text document. - text: str - - -@dataclass -class TextDocumentPositionParams(SerializableStructure): - """ - Data structure which represents a parameter literal used in requests to pass a text document and a position - inside that document. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentPositionParams - """ - # The text document. - text_document: TextDocumentIdentifier - - # The position inside the text document. - position: Position - - -@dataclass -class DocumentFilter(SerializableStructure): - """ - Data structure which represents a document filter which denotes a document through properties like language, - scheme or pattern - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentFilter - """ - # A language id, like `typescript`. - language: Optional[str] - - # A Uri [scheme](#Uri.scheme), like `file` or `untitled`. - scheme: Optional[str] - - # A glob pattern, like `*.{ts,js}`. - # - # Glob patterns can have the following syntax: - # - `*` to match one or more characters in a path segment - # - `?` to match on one character in a path segment - # - `**` to match any number of path segments, including none - # - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` - # matches all TypeScript and JavaScript files) - # - `[]` to declare a range of characters to match in a path segment - # (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - # - `[!...]` to negate a range of characters to match in a path segment - # (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but - # not `example.0`) - pattern: Optional[str] - - -class MarkupKind(Enum): - """ - Represents a string value which content can be represented in different formats - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#markupContent - """ - PLAINTEXT = 'plaintext' - MARKDOWN = 'markdown' - - -@dataclass -class MarkupContent(SerializableStructure): - """ - Data structure which represents a document filter which denotes a document through properties like language, - scheme or pattern - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#markupContentInnerDefinition - """ - # A `MarkupContent` literal represents a string value which content is - # interpreted base on its kind flag. Currently the protocol supports - # `plaintext` and `markdown` as markup kinds. - # - # If the kind is `markdown` then the value can contain fenced code blocks like - # in GitHub issues. - # - # Here is an example how such a string can be constructed using - # JavaScript / TypeScript: - # ```typescript - # let markdown: MarkdownContent = { - # kind: MarkupKind.Markdown, - # value: [ - # '# Header', - # 'Some text', - # '```typescript', - # 'someCode();', - # '```' - # ].join('\n') - # }; - # ``` - # - # *Please Note* that clients might sanitize the return markdown. A client could - # decide to remove HTML from the markdown to avoid script execution. - - # The type of the Markup - kind: MarkupKind - - # The content itself - value: str - - -class TraceValue(Enum): - """ - Defines the level of verbosity to trace server actions with. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#traceValue - """ - OFF = 'off' - MESSAGES = 'messages' - VERBOSE = 'verbose' diff --git a/slither_lsp/lsp/types/capabilities.py b/slither_lsp/lsp/types/capabilities.py deleted file mode 100644 index e90819d..0000000 --- a/slither_lsp/lsp/types/capabilities.py +++ /dev/null @@ -1,795 +0,0 @@ -from dataclasses import dataclass, field -from enum import Enum, IntEnum -from typing import Any, Optional, Union, List - -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure, serialization_metadata -from slither_lsp.lsp.types.basic_structures import DiagnosticTag, DocumentFilter, MarkupKind - - -# region Server Capabilities - - -@dataclass -class WorkDoneProgressOptions(SerializableStructure): - """ - Data structure which represents capabilities to see if work done progress can be tracked. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workDoneProgressOptions - """ - - work_done_progress: Optional[bool] = None - - -@dataclass -class HoverOptions(WorkDoneProgressOptions): - """ - Data structure which represents hover options provided via capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#hoverOptions - """ - # NOTE: This simply inherits from WorkDoneProgressOptions for now - pass - - -@dataclass -class DeclarationOptions(WorkDoneProgressOptions): - """ - Data structure which represents declaration options provided via capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#declarationOptions - """ - # NOTE: This simply inherits from WorkDoneProgressOptions for now - pass - - -@dataclass -class DefinitionOptions(WorkDoneProgressOptions): - """ - Data structure which represents definition options provided via capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#definitionOptions - """ - # NOTE: This simply inherits from WorkDoneProgressOptions for now - pass - - -@dataclass -class TypeDefinitionOptions(WorkDoneProgressOptions): - """ - Data structure which represents type definition options provided via capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#typeDefinitionOptions - """ - # NOTE: This simply inherits from WorkDoneProgressOptions for now - pass - - -@dataclass -class ImplementationOptions(WorkDoneProgressOptions): - """ - Data structure which represents implementation options provided via capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#implementationOptions - """ - # NOTE: This simply inherits from WorkDoneProgressOptions for now - pass - - -@dataclass -class ReferenceOptions(WorkDoneProgressOptions): - """ - Data structure which represents find reference options provided via capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#referenceOptions - """ - # NOTE: This simply inherits from WorkDoneProgressOptions for now - pass - - -@dataclass -class DocumentHighlightOptions(WorkDoneProgressOptions): - """ - Data structure which represents document highlight options provided via capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlightOptions - """ - # NOTE: This simply inherits from WorkDoneProgressOptions for now - pass - - -@dataclass -class WorkspaceFoldersServerCapabilities(SerializableStructure): - """ - Data structure which represents workspace folder specific server capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceFoldersServerCapabilities - """ - # The server has support for workspace folders - supported: Optional[bool] = None - - # Whether the server wants to receive workspace folder - # change notifications. - # - # If a string is provided, the string is treated as an ID - # under which the notification is registered on the client - # side. The ID can be used to unregister for these events - # using the `client/unregisterCapability` request. - change_notifications: Union[str, bool, None] = None - - -@dataclass -class FileOperationPatternKind(Enum): - """ - Defines a pattern kind describing if a glob pattern matches a file a folder or both. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileOperationPatternKind - """ - FILE = 'file' - FOLDER = 'folder' - - -@dataclass -class FileOperationPatternOptions(SerializableStructure): - """ - Data structure which represents matching options for the file operation pattern. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileOperationPatternOptions - """ - ignore_case: Optional[bool] = None - - -@dataclass -class FileOperationPattern(SerializableStructure): - """ - Data structure which represents a pattern to describe in which file operation requests or notifications the - server is interested in. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileOperationPattern - """ - # The glob pattern to match. Glob patterns can have the following syntax: - # - `*` to match one or more characters in a path segment - # - `?` to match on one character in a path segment - # - `**` to match any number of path segments, including none - # - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` - # matches all TypeScript and JavaScript files) - # - `[]` to declare a range of characters to match in a path segment - # (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - # - `[!...]` to negate a range of characters to match in a path segment - # (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but - # not `example.0`) - glob: str - - # Whether to match files or folders with this pattern. - # Matches both if undefined. - matches: Optional[FileOperationPatternKind] - - # Additional options used during matching. - options: Optional[FileOperationPatternOptions] - - -@dataclass -class FileOperationFilter(SerializableStructure): - """ - Data structure which represents a filter to describe in which file operation requests or notifications the - server is interested in. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileOperationFilter - """ - # A Uri like `file` or `untitled`. - scheme: Optional[str] - - # The actual file operation pattern. - pattern: FileOperationPattern - - -@dataclass -class FileOperationRegistrationOptions(SerializableStructure): - """ - Data structure which represents the options to register for file operations. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileOperationRegistrationOptions - """ - # The actual filters. - filters: List[FileOperationFilter] - - -@dataclass -class WorkspaceFileOperationsServerCapabilities(SerializableStructure): - """ - Data structure which represents workspace file operation server capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#serverCapabilities - """ - # The server is interested in receiving didCreateFiles notifications. - did_create: Optional[FileOperationRegistrationOptions] - - # The server is interested in receiving willCreateFiles requests. - will_create: Optional[FileOperationRegistrationOptions] - - # The server is interested in receiving didRenameFiles notifications. - did_rename: Optional[FileOperationRegistrationOptions] - - # The server is interested in receiving willRenameFiles requests. - will_rename: Optional[FileOperationRegistrationOptions] - - # The server is interested in receiving didDeleteFiles file notifications. - did_delete: Optional[FileOperationRegistrationOptions] - - # The server is interested in receiving willDeleteFiles file requests. - will_delete: Optional[FileOperationRegistrationOptions] - - -@dataclass -class WorkspaceServerCapabilities(SerializableStructure): - """ - Data structure which represents workspace specific server capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#serverCapabilities - """ - # The server supports workspace folder. - # @since 3.6.0 - workspace_folders: Optional[WorkspaceFoldersServerCapabilities] = None - - # The server is interested in file notifications/requests. - # @since 3.16.0 - file_operations: Optional[WorkspaceFileOperationsServerCapabilities] = None - - -class TextDocumentSyncKind(IntEnum): - """ - Defines how the host (editor) should sync document changes to the language server. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentSyncKind - """ - # Documents should not be synced at all. - NONE = 0 - - # Documents are synced by always sending the full content of the document. - FULL = 1 - - # Documents are synced by sending the full content on open. - # After that only incremental updates to the document are - # send. - INCREMENTAL = 2 - - -@dataclass -class SaveOptions(SerializableStructure): - """ - Data structure which represents options for a saved file - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#saveOptions - """ - # The client is supposed to include the content on save. - include_text: Optional[bool] = None - - -@dataclass -class TextDocumentSyncOptions(SerializableStructure): - """ - Data structure which represents options to delete a file. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentSyncOptions - Note: - There are two structs defined in the referenced documentation. One partial, one full. - """ - # Open and close notifications are sent to the server. If omitted open close notification should not be sent. - open_close: Optional[bool] = None - - # Change notifications are sent to the server. See - # TextDocumentSyncKind.None, TextDocumentSyncKind.Full and - # TextDocumentSyncKind.Incremental. If omitted it defaults to - # TextDocumentSyncKind.None. - change: Optional[TextDocumentSyncKind] = None - - # If present will save notifications are sent to the server. If omitted - # the notification should not be sent. - will_save: Optional[bool] = None - - # If present will save wait until requests are sent to the server. If - # omitted the request should not be sent. - will_save_wait_until: Optional[bool] = None - - # If present save notifications are sent to the server. If omitted the - # notification should not be sent. - save: Union[bool, SaveOptions, None] = None - - -@dataclass -class ServerCapabilities(SerializableStructure): - """ - Data structure which represents capabilities a server supports. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#serverCapabilities - """ - # Defines how text documents are synced. Is either a detailed structure - # defining each notification or for backwards compatibility the - # TextDocumentSyncKind number. If omitted it defaults to - # `TextDocumentSyncKind.None`. - text_document_sync: Union[TextDocumentSyncOptions, TextDocumentSyncKind, None] = None - - # The server provides hover support. - hover_provider: Union[bool, HoverOptions, None] = None - - # The server provides go to declaration support. - # @since 3.14.0 - # TODO: Once we add DeclarationRegistrationOptions, we need to add logic for it here, as it should be another - # possible value type for this field. - declaration_provider: Union[bool, DeclarationOptions, None] = None - - # The server provides goto definition support. - definition_provider: Union[bool, DefinitionOptions, None] = None - - # The server provides goto type definition support. - # @since 3.6.0 - # TODO: Once we add TypeDefinitionRegistrationOptions, we need to add logic for it here, as it should be another - # possible value type for this field. - type_definition_provider: Union[bool, TypeDefinitionOptions, None] = None - - # The server provides goto implementation support. - # @since 3.6.0 - # TODO: Once we add ImplementationRegistrationOptions, we need to add logic for it here, as it should be another - # possible value type for this field. - implementation_provider: Union[bool, ImplementationOptions, None] = None - - # The server provides find references support. - references_provider: Union[bool, ReferenceOptions, None] = None - - # The server provides document highlight support. - document_highlight_provider: Union[bool, DocumentHighlightOptions, None] = None - - # Workspace specific server capabilities - workspace: Optional[WorkspaceServerCapabilities] = None - -# endregion - - -# region Client Capabilities - -@dataclass -class ShowDocumentClientCapabilities(SerializableStructure): - """ - Data structure which represents capabilities for the request to display a document. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#window_showDocument - """ - # The client has support for the show document request. - support: bool = False - - -@dataclass -class WindowClientCapabilities(SerializableStructure): - """ - Data structure which represents window specific client capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#clientCapabilities - """ - # Whether client supports handling progress notifications. If set - # servers are allowed to report in `workDoneProgress` property in the - # request specific server capabilities. - # @since 3.15.0 - work_done_progress: Optional[bool] = None - - # TODO: showMessage - - # Client capabilities for the show document request. - # @since 3.16.0 - show_document: Optional[ShowDocumentClientCapabilities] = None - - -@dataclass -class PublishDiagnosticsTagSupportClientCapabilities(SerializableStructure): - """ - Data structure which contains tag support information ('tagSupport' in 'PublishDiagnosticsClientCapabilities') - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#publishDiagnosticsClientCapabilities - """ - # Whether the clients accepts diagnostics with related information. - value_set: List[DiagnosticTag] = field(default_factory=list) - - -@dataclass -class PublishDiagnosticsClientCapabilities(SerializableStructure): - """ - Data structure which represents text document specific client capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#publishDiagnosticsClientCapabilities - """ - # Whether the clients accepts diagnostics with related information. - related_information: Optional[bool] = None - - # Client supports the tag property to provide meta data about a diagnostic. - # Clients supporting tags have to handle unknown tags gracefully. - # @since 3.15.0 - tag_support: Optional[PublishDiagnosticsTagSupportClientCapabilities] = None - - # Whether the client interprets the version property of the - # `textDocument/publishDiagnostics` notification's parameter. - # @since 3.15.0 - version_support: Optional[bool] = None - - # Client supports a codeDescription property - # @since 3.16.0 - code_description_support: Optional[bool] = None - - # Whether code action supports the `data` property which is - # preserved between a `textDocument/publishDiagnostics` and - # `textDocument/codeAction` request. - # @since 3.16.0 - data_support: Optional[bool] = None - - -@dataclass -class HoverClientCapabilities(SerializableStructure): - """ - Data structure which contains capabilities specific to the 'textDocument/hover' request. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#hoverClientCapabilities - """ - # Whether hover supports dynamic registration. - dynamic_registration: Optional[bool] = None - - # Client supports the follow content formats if the content - # property refers to a `literal of type MarkupContent`. - # The order describes the preferred format of the client. - content_format: Optional[List[MarkupKind]] = None - - -@dataclass -class DeclarationClientCapabilities(SerializableStructure): - """ - Data structure which contains capabilities specific to the 'textDocument/declaration' request. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentClientCapabilities - """ - # Whether declaration supports dynamic registration. If this is set to - # `true` the client supports the new `DeclarationRegistrationOptions` - # return value for the corresponding server capability as well. - dynamic_registration: Optional[bool] = None - - # The client supports additional metadata in the form of declaration links. - link_support: Optional[bool] = None - - -@dataclass -class DefinitionClientCapabilities(SerializableStructure): - """ - Data structure which contains capabilities specific to the 'textDocument/definition' request. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#definitionClientCapabilities - """ - # Whether definition supports dynamic registration. - dynamic_registration: Optional[bool] = None - - # The client supports additional metadata in the form of definition links. - # @since 3.14.0 - link_support: Optional[bool] = None - - -@dataclass -class TypeDefinitionClientCapabilities(SerializableStructure): - """ - Data structure which contains capabilities specific to the 'textDocument/typeDefinition' request. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#typeDefinitionClientCapabilities - """ - # Whether implementation supports dynamic registration. If this is set to - # `true` the client supports the new `TypeDefinitionRegistrationOptions` - # return value for the corresponding server capability as well. - dynamic_registration: Optional[bool] = None - - # The client supports additional metadata in the form of definition links. - # @since 3.14.0 - link_support: Optional[bool] = None - - -@dataclass -class ImplementationClientCapabilities(SerializableStructure): - """ - Data structure which contains capabilities specific to the 'textDocument/implementation' request. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#implementationClientCapabilities - """ - # Whether implementation supports dynamic registration. If this is set to - # `true` the client supports the new `ImplementationRegistrationOptions` - # return value for the corresponding server capability as well. - dynamic_registration: Optional[bool] = None - - # The client supports additional metadata in the form of definition links. - # @since 3.14.0 - link_support: Optional[bool] = None - - -@dataclass -class ReferenceClientCapabilities(SerializableStructure): - """ - Data structure which contains capabilities specific to the 'textDocument/references' request. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#referenceClientCapabilities - """ - # Whether references supports dynamic registration. - dynamic_registration: Optional[bool] = None - - -@dataclass -class DocumentHighlightClientCapabilities(SerializableStructure): - """ - Data structure which contains capabilities specific to the 'textDocument/documentHighlight' request. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_documentHighlight - """ - # Whether document highlight supports dynamic registration. - dynamic_registration: Optional[bool] = None - - -@dataclass -class TextDocumentSyncClientCapabilities(SerializableStructure): - """ - Data structure which contains capabilities specific to the text document synchronization. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentSyncClientCapabilities - """ - # Whether text document synchronization supports dynamic registration. - dynamic_registration: Optional[bool] = None - - # The client supports sending will save notifications. - will_save: Optional[bool] = None - - # The client supports sending a will save request and - # waits for a response providing text edits which will - # be applied to the document before it is saved. - will_save_wait_until: Optional[bool] = None - - # The client supports did save notifications. - did_save: Optional[bool] = None - - -@dataclass -class TextDocumentClientCapabilities(SerializableStructure): - """ - Data structure which represents text document specific client capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentClientCapabilities - """ - # Text synchronization capabilities - synchronization: Optional[TextDocumentSyncClientCapabilities] = None - - # TODO: completion - - # Capabilities specific to the `textDocument/hover` request. - hover: Optional[HoverClientCapabilities] = None - - # TODO: signatureHelp - - # Capabilities specific to the `textDocument/declaration` request. - # @since 3.14.0 - declaration: Optional[DeclarationClientCapabilities] = None - - # Capabilities specific to the `textDocument/definition` request. - definition: Optional[DefinitionClientCapabilities] = None - - # Capabilities specific to the `textDocument/typeDefinition` request. - # @since 3.6.0 - type_definition: Optional[TypeDefinitionClientCapabilities] = None - - # Capabilities specific to the `textDocument/implementation` request. - # @since 3.6.0 - implementation: Optional[ImplementationClientCapabilities] = None - - # Capabilities specific to the `textDocument/references` request. - references: Optional[ReferenceClientCapabilities] = None - - # Capabilities specific to the `textDocument/publishDiagnostics` notification. - publish_diagnostics: Optional[PublishDiagnosticsClientCapabilities] = None - - # Capabilities specific to the `textDocument/documentHighlight` request. - document_highlight: Optional[DocumentHighlightClientCapabilities] = None - - -class ResourceOperationKind(Enum): - """ - Defines the kind of resource operations supported by a client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#resourceOperationKind - """ - # Supports creating new files and folders. - CREATE = 'create' - - # Supports renaming existing files and folders. - RENAME = 'rename' - - # Supports deleting existing files and folders. - DELETE = 'delete' - - -class FailureHandlingKind(Enum): - """ - Defines the kind of failure handling supported by a client. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#failureHandlingKind - """ - # Applying the workspace change is simply aborted if one of the changes - # provided fails. All operations executed before the failing operation - # stay executed. - ABORT = 'abort' - - # All operations are executed transactional. That means they either all - # succeed or no changes at all are applied to the workspace. - TRANSACTIONAL = 'transactional' - - # If the workspace edit contains only textual file changes they are - # executed transactional. If resource changes (create, rename or delete - # file) are part of the change the failure handling strategy is abort. - TEXT_ONLY_TRANSACTIONAL = 'textOnlyTransactional' - - # The client tries to undo the operations already executed. But there is no - # guarantee that this is succeeding. - UNDO = 'undo' - - -@dataclass -class WorkspaceEditChangeAnnotationSupportClientCapabilities(SerializableStructure): - """ - Data structure which describe a subsection of a client's workspace edit capabilities related to change annotation - support. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceEditClientCapabilities - """ - # Whether the client groups edits with equal labels into tree nodes, - # for instance all edits labelled with "Changes in Strings" would - # be a tree node. - groups_on_label: Optional[bool] - - -@dataclass -class WorkspaceEditClientCapabilities(SerializableStructure): - """ - Data structure which describe a clients capabilities for workspace edits. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceEditClientCapabilities - """ - # - document_changes: Optional[bool] - - resource_operations: Optional[List[ResourceOperationKind]] - - failure_handling: Optional[FailureHandlingKind] - - normalizes_line_endings: Optional[bool] - - change_annotation_support: Optional[WorkspaceEditChangeAnnotationSupportClientCapabilities] - - -@dataclass -class DidChangeWatchedFilesClientCapabilities(SerializableStructure): - """ - Data structure which describe a clients capabilities for reporting changes to watched files. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didChangeWatchedFilesClientCapabilities - """ - # Did change watched files notification supports dynamic registration. - # Please note that the current protocol doesn't support static - # configuration for file changes from the server side. - dynamic_registration: Optional[bool] = None - - -@dataclass -class WorkspaceFileOperationsClientCapabilities(SerializableStructure): - """ - Data structure which represents a subsection of client capabilities for file requests/notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#clientCapabilities - """ - # Whether the client supports dynamic registration for file - # requests/notifications. - dynamic_registration: Optional[bool] - - # The client has support for sending didCreateFiles notifications. - did_create: Optional[bool] - - # The client has support for sending willCreateFiles requests. - will_create: Optional[bool] - - # The client has support for sending didRenameFiles notifications. - did_rename: Optional[bool] - - # The client has support for sending willRenameFiles requests. - will_rename: Optional[bool] - - # The client has support for sending didDeleteFiles notifications. - did_delete: Optional[bool] - - # The client has support for sending willDeleteFiles requests. - will_delete: Optional[bool] - - -@dataclass -class WorkspaceClientCapabilities(SerializableStructure): - """ - Data structure which represents workspace specific client capabilities. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#clientCapabilities - """ - # The client supports applying batch edits - # to the workspace by supporting the request - # 'workspace/applyEdit' - apply_edit: Optional[bool] = None - - # Capabilities specific to `WorkspaceEdit`s - workspace_edit: Optional[WorkspaceEditClientCapabilities] = None - - # TODO: workspaceEdit, didChangeConfiguration - - # Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - did_change_watched_files: Optional[DidChangeWatchedFilesClientCapabilities] = None - - # TODO: symbol, executeCommand - - # The client has support for workspace folders. - # @since 3.6.0 - workspace_folders: Optional[bool] = None - - # The client supports `workspace/configuration` requests. - # @since 3.6.0 - configuration: Optional[bool] = None - - # TODO: semanticTokens, codeLens - - # The client has support for file requests/notifications. - # @since 3.16.0 - file_operations: Optional[WorkspaceFileOperationsClientCapabilities] = None - - # TODO: textDocument, window, general, - - # Experimental client capabilities. - experimental: Any = None - - -@dataclass -class MarkdownClientCapabilities(SerializableStructure): - """ - Data structure which represents client capabilities specific to the used markdown parser. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#markdownClientCapabilities - """ - # The name of the parser. - parser: str - - # The version of the parser. - version: Optional[str] - - -@dataclass -class GeneralClientCapabilities(SerializableStructure): - """ - Data structure which represents a subsection of client capabilities - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#clientCapabilities - """ - # Client capabilities specific to the client's markdown parser. - # @since 3.16.0 - markdown: Optional[MarkdownClientCapabilities] - - -@dataclass -class ClientCapabilities(SerializableStructure): - """ - Data structure which represents capabilities a client supports. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#clientCapabilities - """ - # Workspace specific client capabilities. - workspace: Optional[WorkspaceClientCapabilities] = None - - # Window specific client capabilities. - window: Optional[WindowClientCapabilities] = None - - # Text document specific client capabilities. - text_document: Optional[TextDocumentClientCapabilities] = None - - # General client capabilities. - # @since 3.16.0 - general: Optional[GeneralClientCapabilities] = None - -# endregion - diff --git a/slither_lsp/lsp/types/errors.py b/slither_lsp/lsp/types/errors.py deleted file mode 100644 index 1da03a2..0000000 --- a/slither_lsp/lsp/types/errors.py +++ /dev/null @@ -1,90 +0,0 @@ -from enum import IntEnum -from typing import Any, Union, Type, Optional - -# pylint: disable=invalid-name -from slither_lsp.lsp.request_handlers.base_handler import BaseRequestHandler -from slither_lsp.lsp.requests.base_request import BaseRequest - - -class LSPErrorCode(IntEnum): - """ - Defines a set of error codes for use with the Language Server Protocol. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#responseMessage - """ - # Defined by JSON RPC - ParseError = -32700 - InvalidRequest = -32600 - MethodNotFound = -32601 - InvalidParams = -32602 - InternalError = -32603 - - # This is the start range of JSON RPC reserved error codes. - # It doesn't denote a real error code. No LSP error codes should - # be defined between the start and end range. For backwards - # compatibility the `ServerNotInitialized` and the `UnknownErrorCode` - # are left in the range. - # @since 3.16.0 - jsonrpcReservedErrorRangeStart = -32099 - # @deprecated use jsonrpcReservedErrorRangeStart - serverErrorStart = jsonrpcReservedErrorRangeStart - - # Error code indicating that a server received a notification or - # request before the server has received the `initialize` request. - ServerNotInitialized = -32002 - UnknownErrorCode = -32001 - - # This is the start range of JSON RPC reserved error codes. - # It doesn't denote a real error code. - # @since 3.16.0 - jsonrpcReservedErrorRangeEnd = -32000 - # @deprecated use jsonrpcReservedErrorRangeEnd - serverErrorEnd = jsonrpcReservedErrorRangeEnd - - # This is the start range of LSP reserved error codes. - # It doesn't denote a real error code. - # @since 3.16.0 - lspReservedErrorRangeStart = -32899 - - ContentModified = -32801 - RequestCancelled = -32800 - - # This is the end range of LSP reserved error codes. - # It doesn't denote a real error code. - # @since 3.16.0 - lspReservedErrorRangeEnd = -32800 - - -class LSPError(Exception): - """ - Represents an LSP error, which when thrown in a command handler will be sent to the LSP client. - """ - def __init__(self, code: LSPErrorCode, message: str, data: Any = None): - self.error_code = code - self.error_message = message - self.error_data = data - super().__init__() - - -class CapabilitiesNotSupportedError(LSPError): - """ - Represents an exception which is thrown when a command (request/notification) is invoked but is not supported - by the client or server. - """ - def __init__( - self, - request_or_handler: Union[BaseRequest, Type[BaseRequest], BaseRequestHandler, Type[BaseRequestHandler]], - data: Any = None, - additional_text: Optional[str] = None - ): - # Construct our message - text = f"'{request_or_handler.method_name}' is not supported due to client/server capabilities." - if additional_text is not None: - text += " " + additional_text - - # Constructor our underlying LSP Error. - super().__init__( - LSPErrorCode.InternalError, - text, - data - ) \ No newline at end of file diff --git a/slither_lsp/lsp/types/params.py b/slither_lsp/lsp/types/params.py deleted file mode 100644 index 0c6c37b..0000000 --- a/slither_lsp/lsp/types/params.py +++ /dev/null @@ -1,634 +0,0 @@ -# pylint: disable=duplicate-code -from dataclasses import dataclass, field -from enum import IntEnum -from typing import Union, Any, Optional, List - -from slither_lsp.lsp.types.capabilities import ClientCapabilities, ServerCapabilities -from slither_lsp.lsp.types.basic_structures import ClientServerInfo, TraceValue, WorkspaceFolder, MessageType, Range, \ - Diagnostic, TextDocumentIdentifier, Position, TextDocumentItem, VersionedTextDocumentIdentifier, MarkupContent -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure - - -@dataclass -class PartialResultParams(SerializableStructure): - """ - Data structure which represents a parameter literal used to pass a partial result token. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#partialResultParams - """ - # An optional token that a server can use to report partial results (e.g. streaming) to the client. - partial_result_token: Union[str, int, None] = None - - -@dataclass -class WorkDoneProgressParams(SerializableStructure): - """ - Data structure which represents a work done progress token. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workDoneProgressParams - """ - # An optional token that a server can use to report work done progress. - work_done_token: Union[str, int, None] = None - - -@dataclass -class InitializeParams(WorkDoneProgressParams): - """ - Data structure which represents 'initialize' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#initializeParams - """ - # The process Id of the parent process that started the server. Is null if - # the process has not been started by another process. If the parent - # process is not alive then the server should exit (see exit notification) - # its process. - process_id: Optional[int] = None - - # Information about the client - client_info: Optional[ClientServerInfo] = None - - # The locale the client is currently showing the user interface - # in. This must not necessarily be the locale of the operating - # system. - # - # Uses IETF language tags as the value's syntax - # (See https://en.wikipedia.org/wiki/IETF_language_tag) - # @since 3.16.0 - locale: Optional[str] = None - - # The rootPath of the workspace. Is null - # if no folder is open. - # - # @deprecated in favour of `rootUri`. - root_path: Optional[str] = None - - # The rootUri of the workspace. Is null if no - # folder is open. If both `rootPath` and `rootUri` are set - # `rootUri` wins. - # - # @deprecated in favour of `workspaceFolders` - root_uri: Optional[str] = None - - # User provided initialization options. - initialization_options: Any = None - - # The capabilities provided by the client (editor or tool) - capabilities: ClientCapabilities = field(default_factory=ClientCapabilities) - - # The initial trace setting. If omitted trace is disabled ('off'). - trace: Optional[TraceValue] = None - - # The workspace folders configured in the client when the server starts. - # This property is only available if the client supports workspace folders. - # It can be `null` if the client supports workspace folders but none are - # configured. - # @since 3.6.0 - workspace_folders: Optional[List[WorkspaceFolder]] = field(default_factory=list) - - -@dataclass -class InitializeResult(SerializableStructure): - """ - Data structure which represents 'initialize' responses. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#initializeResult - """ - # The capabilities the language server provides. - capabilities: ServerCapabilities = field(default_factory=ServerCapabilities) - - # Information about the server. - server_info: Optional[ClientServerInfo] = None - - -@dataclass -class SetTraceParams(SerializableStructure): - """ - Data structure which represents '$/setTrace' notification parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#setTrace - """ - # The new value that should be assigned to the trace setting. - value: TraceValue = TraceValue.OFF - - -@dataclass -class ShowMessageParams(SerializableStructure): - """ - Data structure which represents 'window/showMessage' requests. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#window_showMessage - """ - # The message type. - type: MessageType = MessageType.LOG - - # The actual message. - message: str = "" - - -@dataclass -class ShowDocumentParams(SerializableStructure): - """ - Data structure which represents 'window/showDocument' requests. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#window_showDocument - """ - # The document uri to show. - uri: str = "" - - # Indicates to show the resource in an external program. - # To show for example `https://code.visualstudio.com/` - # in the default WEB browser set `external` to `true`. - external: Optional[bool] = None - - # An optional property to indicate whether the editor - # showing the document should take focus or not. - # Clients might ignore this property if an external - # program is started. - take_focus: Optional[bool] = None - - # An optional selection range if the document is a text - # document. Clients might ignore the property if an - # external program is started or the file is not a text - # file. - selection: Optional[Range] = None - - -@dataclass -class ShowDocumentResult(SerializableStructure): - """ - Data structure which represents 'window/showDocument' responses. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#window_showDocument - """ - # A boolean indicating if the show was successful. - success: bool = False - - -@dataclass -class LogMessageParams(SerializableStructure): - """ - Data structure which represents 'window/logMessage' requests. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#window_logMessage - """ - # The message type. - type: MessageType = MessageType.LOG - - # The actual message. - message: str = "" - - -@dataclass -class Registration(SerializableStructure): - """ - Data structure which represents general parameters to register for a capability. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#registration - """ - # The id used to register the request. The id can be used to deregister the request again. - id: str - - # The method / capability to register for. - method: str - - # Options necessary for the registration. - register_options: Any - - -@dataclass -class RegistrationParams(SerializableStructure): - """ - Data structure which represents 'client/registerCapability' requests. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#registrationParams - """ - registrations: List[Registration] - - -@dataclass -class Unregistration(SerializableStructure): - """ - Data structure which represents general parameters to unregister a capability. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#unregistration - """ - # The id used to unregister the request or notification. Usually an id provided during the register request. - id: str - - # The method / capability to unregister for. - method: str - - -@dataclass -class UnregistrationParams(SerializableStructure): - """ - Data structure which represents 'client/unregisterCapability' requests. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#unregistrationParams - """ - # This should correctly be named `unregistrations`. However changing this - # is a breaking change and needs to wait until we deliver a 4.x version - # of the specification. - unregisterations: List[Unregistration] - - -@dataclass -class WorkspaceFoldersChangeEvent(SerializableStructure): - """ - Data structure which represents workspace folder change event data. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceFoldersChangeEvent - """ - # The array of added workspace folders - added: List[WorkspaceFolder] = field(default_factory=list) - - # The array of the removed workspace folder - removed: List[WorkspaceFolder] = field(default_factory=list) - - -@dataclass -class DidChangeWorkspaceFoldersParams(SerializableStructure): - """ - Data structure which represents 'workspace/didChangeWorkspaceFolders' notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didChangeWorkspaceFoldersParams - """ - # The actual workspace folder change event. - event: WorkspaceFoldersChangeEvent = field(default_factory=WorkspaceFoldersChangeEvent) - - -class FileChangeType(IntEnum): - """ - The file event type. - """ - # The file got created. - CREATED = 1 - - # The file got changed. - CHANGED = 2 - - # The file got deleted. - DELETED = 3 - - -@dataclass -class FileEvent(SerializableStructure): - """ - Data structure which represents an event describing a file change. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileEvent - """ - # The file's URI. - uri: str - - # The change type. - type: FileChangeType - - -@dataclass -class DidChangeWatchedFilesParams(SerializableStructure): - """ - Data structure which represents 'workspace/didChangeWatchedFiles' notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didChangeWatchedFilesParams - """ - # The actual file events. - changes: List[FileEvent] - - -@dataclass -class FileCreate(SerializableStructure): - """ - Data structure which represents information on a file/folder create. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileCreate - """ - # A file:// URI for the location of the file/folder being created. - uri: str - - -@dataclass -class CreateFilesParams(SerializableStructure): - """ - Data structure which represents the parameters sent in notifications/requests for user-initiated creation of files. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#createFilesParams - """ - # An array of all files/folders created in this operation. - files: List[FileCreate] - - -@dataclass -class FileRename(SerializableStructure): - """ - Data structure which represents information on a file/folder rename. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileRename - """ - # A file:// URI for the original location of the file/folder being renamed. - old_uri: str - - # A file:// URI for the new location of the file/folder being renamed. - new_uri: str - - -@dataclass -class RenameFilesParams(SerializableStructure): - """ - Data structure which represents the parameters sent in notifications/requests for user-initiated renames of files. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#renameFilesParams - """ - # An array of all files/folders renamed in this operation. When a folder is renamed, only the folder will be - # included, and not its children. - files: List[FileRename] - - -@dataclass -class FileDelete(SerializableStructure): - """ - Data structure which represents information on a file/folder delete. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#fileDelete - """ - # A file:// URI for the location of the file/folder being deleted. - uri: str - - -@dataclass -class DeleteFilesParams(SerializableStructure): - """ - Data structure which represents the parameters sent in notifications/requests for user-initiated deletes of files. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#deleteFilesParams - """ - # An array of all files/folders deleted in this operation. - files: List[FileDelete] - - -@dataclass -class DidOpenTextDocumentParams(SerializableStructure): - """ - Data structure which represents 'textDocument/didOpen' notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didOpenTextDocumentParams - """ - # The document that was opened. - text_document: TextDocumentItem - - -@dataclass -class TextDocumentContentChangeEvent(SerializableStructure): - """ - Data structure which represents an event describing a change to a text document. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentContentChangeEvent - """ - # NOTE: This class should either just have 'text', or 'text', 'range', and an optional 'rangeLength'. - # For this reason, we just make 'range' optional here since we're simply receiving it. Otherwise we'd need to - # define two different structs. - - # The range of the document that changed. - text: str - - # The optional length of the range that got replaced. - # @deprecated use range instead. - range: Optional[Range] = None - - range_length: Optional[int] = None - - -@dataclass -class DidChangeTextDocumentParams(SerializableStructure): - """ - Data structure which represents 'textDocument/didChange' notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didChangeTextDocumentParams - """ - # The document that did change. The version number points - # to the version after all provided content changes have - # been applied. - text_document: VersionedTextDocumentIdentifier - - # The actual content changes. The content changes describe single state - # changes to the document. So if there are two content changes c1 (at - # array index 0) and c2 (at array index 1) for a document in state S then - # c1 moves the document from S to S' and c2 from S' to S''. So c1 is - # computed on the state S and c2 is computed on the state S'. - # - # To mirror the content of a document using change events use the following - # approach: - # - start with the same initial content - # - apply the 'textDocument/didChange' notifications in the order you - # receive them. - # - apply the `TextDocumentContentChangeEvent`s in a single notification - # in the order you receive them. - content_changes: List[TextDocumentContentChangeEvent] - - -class TextDocumentSaveReason(IntEnum): - """ - Defines how the host (editor) should sync document changes to the language server. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentSyncKind - """ - # Manually triggered, e.g. by the user pressing save, by starting debugging, or by an API call. - MANUAL = 1 - - # Automatic after a delay. - AFTER_DELAY = 2 - - # When the editor lost focus. - FOCUS_OUT = 3 - - -@dataclass -class WillSaveTextDocumentParams(SerializableStructure): - """ - Data structure which represents 'textDocument/willSave' notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#willSaveTextDocumentParams - """ - # The document that will be saved. - text_document: TextDocumentIdentifier - - # The 'TextDocumentSaveReason'. - reason: TextDocumentSaveReason - - -@dataclass -class DidSaveTextDocumentParams(SerializableStructure): - """ - Data structure which represents 'textDocument/didSave' notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didSaveTextDocumentParams - """ - # The document that was saved. - text_document: TextDocumentIdentifier - - # Optional the content when saved. Depends on the includeText value when the save notification was requested. - text: Optional[str] = None - - -@dataclass -class DidCloseTextDocumentParams(SerializableStructure): - """ - Data structure which represents 'textDocument/didClose' notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didCloseTextDocumentParams - """ - # The document that was closed. - text_document: TextDocumentIdentifier - - -@dataclass -class PublishDiagnosticsParams(SerializableStructure): - - """ - Data structure which represents 'textDocument/publishDiagnostics' notifications. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#publishDiagnosticsParams - """ - uri: str = "" - version: Optional[int] = None - diagnostics: List[Diagnostic] = field(default_factory=list) - - -@dataclass -class TextDocumentPositionParams(SerializableStructure): - """ - Data structure which represents 'initialize' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentPositionParams - """ - # The text document - text_document: TextDocumentIdentifier = field(default_factory=TextDocumentIdentifier) - - # The position inside the text document. - position: Position = field(default_factory=Position) - - -@dataclass -class HoverParams(TextDocumentPositionParams, WorkDoneProgressParams): - """ - Data structure which represents 'textDocument/hover' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#hoverParams - """ - # Note: For now this just inherits from its base classes. - pass - - -@dataclass -class HoverMarkedStringLanguageValue(SerializableStructure): - """ - Data structure which represents a subsection of 'textDocument/hover' response parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#hover - """ - language: str - value: str - - -@dataclass -class Hover(SerializableStructure): - """ - Data structure which represents 'textDocument/hover' response parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#hover - """ - # The hover's content. - contents: Union[HoverMarkedStringLanguageValue, str, List[Union[HoverMarkedStringLanguageValue, str]], MarkupContent] = field( - default_factory=list - ) - - # An optional range is a range inside a text document - # that is used to visualize a hover, e.g. by changing the background color. - range: Optional[Range] = None - - -@dataclass -class DeclarationParams(TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams): - """ - Data structure which represents 'textDocument/declaration' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#declarationParams - """ - # Note: For now this just inherits from its base classes. - pass - - -@dataclass -class DefinitionParams(TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams): - """ - Data structure which represents 'textDocument/definition' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#definitionParams - """ - # Note: For now this just inherits from its base classes. - pass - - -@dataclass -class TypeDefinitionParams(TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams): - """ - Data structure which represents 'textDocument/typeDefinition' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#typeDefinitionParams - """ - # Note: For now this just inherits from its base classes. - pass - - -@dataclass -class ImplementationParams(TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams): - """ - Data structure which represents 'textDocument/implementation' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#implementationParams - """ - # Note: For now this just inherits from its base classes. - pass - - -@dataclass -class ReferenceContext(SerializableStructure): - """ - Data structure which represents 'textDocument/references' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#referenceContext - """ - # Include the declaration of the current symbol. - include_declaration: bool = False - - -@dataclass -class ReferenceParams(TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams): - """ - Data structure which represents 'textDocument/references' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#referenceParams - """ - context: ReferenceContext = field(default_factory=ReferenceContext) - - -@dataclass -class DocumentHighlightParams(TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams): - """ - Data structure which represents 'textDocument/documentHighlight' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlightParams - """ - # Note: For now this just inherits from its base classes. - pass - - -@dataclass -class MonikerParams(TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams): - """ - Data structure which represents 'textDocument/moniker' request parameters. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#monikerParams - """ - # Note: For now this just inherits from its base classes. - pass diff --git a/slither_lsp/lsp/types/registration_options.py b/slither_lsp/lsp/types/registration_options.py deleted file mode 100644 index ea0a1d9..0000000 --- a/slither_lsp/lsp/types/registration_options.py +++ /dev/null @@ -1,67 +0,0 @@ -from dataclasses import dataclass, field -from enum import IntFlag -from typing import Optional, List - -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure, serialization_metadata -from slither_lsp.lsp.types.basic_structures import DocumentFilter - - -@dataclass -class TextDocumentRegistrationOptions(SerializableStructure): - """ - Data structure which represents general text document registration options. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentRegistrationOptions - """ - documentSelector: Optional[List[DocumentFilter]] = field( - default=None, - metadata=serialization_metadata(include_none=True) - ) # DocumentSelector | null - - -class WatchKind(IntFlag): - """ - Represents the type of operations a server may signal to a client that it is interested in watching. - """ - CREATE = 1 - CHANGE = 2 - DELETE = 4 - - -@dataclass -class FileSystemWatcher(SerializableStructure): - """ - Data structure which represents registration options for watching files. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didChangeWatchedFilesRegistrationOptions - """ - # The glob pattern to watch. - # - # Glob patterns can have the following syntax: - # - `*` to match one or more characters in a path segment - # - `?` to match on one character in a path segment - # - `**` to match any number of path segments, including none - # - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` - # matches all TypeScript and JavaScript files) - # - `[]` to declare a range of characters to match in a path segment - # (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - # - `[!...]` to negate a range of characters to match in a path segment - # (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not - # `example.0`) - glob_pattern: str - - # The kind of events of interest. If omitted it defaults - # to WatchKind.Create | WatchKind.Change | WatchKind.Delete - # which is 7. - kind: Optional[WatchKind] - - -@dataclass -class DidChangeWatchedFilesRegistrationOptions(SerializableStructure): - """ - Data structure which describe options to be used when registering for file system change events. - References: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#didChangeWatchedFilesRegistrationOptions - """ - # The watchers to register. - watchers: List[FileSystemWatcher] diff --git a/tests/test_lsp_basic_structures.py b/tests/test_lsp_basic_structures.py deleted file mode 100644 index 0abaee1..0000000 --- a/tests/test_lsp_basic_structures.py +++ /dev/null @@ -1,297 +0,0 @@ -import json -from slither_lsp.lsp.types.basic_structures import ClientServerInfo, WorkspaceFolder, Position, Range, Location, \ - LocationLink, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Diagnostic, CodeDescription, \ - Command, TextEdit, AnnotatedTextEdit, ChangeAnnotation - - -def test_client_server_info(): - # Create our expected variables we'll construct tests with. - expected_name = "test_client_name" - expected_version = "1.2.3" - - # Test parsing with just a name - info: ClientServerInfo = ClientServerInfo.from_dict({'name': expected_name}) - assert info.name == expected_name and info.version is None - - # Test round trip conversion - info_copy = ClientServerInfo.from_dict(info.to_dict()) - assert info == info_copy - assert json.dumps(info.to_dict()) == json.dumps(info_copy.to_dict()) - - # Test parsing with a name and version - info: ClientServerInfo = ClientServerInfo.from_dict({'name': expected_name, 'version': expected_version}) - assert info.name == expected_name and info.version == expected_version - - # Test round trip conversion - info_copy = ClientServerInfo.from_dict(info.to_dict()) - assert info == info_copy - assert json.dumps(info.to_dict()) == json.dumps(info_copy.to_dict()) - - -def test_workspace_folder(): - # Create our expected variables we'll construct tests with. - workspace_uri = "/test/directory/OK" - workspace_name = "OK" - - # Test parsing workspace folder with just a uri - workspace_folder: WorkspaceFolder = WorkspaceFolder.from_dict({'uri': workspace_uri}) - assert workspace_folder.uri == workspace_uri - - # Test round trip conversion of a workspace folder with just a uri - workspace_folder_copy = WorkspaceFolder.from_dict(workspace_folder.to_dict()) - assert workspace_folder == workspace_folder_copy - assert json.dumps(workspace_folder.to_dict()) == json.dumps(workspace_folder_copy.to_dict()) - - # Test parsing workspace folder with just a uri and name - workspace_folder: WorkspaceFolder = WorkspaceFolder.from_dict({'uri': workspace_uri, 'name': workspace_name}) - assert workspace_folder.uri == workspace_uri and workspace_folder.name == workspace_name - - # Test round trip conversion of a workspace folder with uri and name - workspace_folder_copy = WorkspaceFolder.from_dict(workspace_folder.to_dict()) - assert workspace_folder == workspace_folder_copy - assert json.dumps(workspace_folder.to_dict()) == json.dumps(workspace_folder_copy.to_dict()) - - -def test_position(): - # Create our expected variables we'll construct tests with. - expected_line = 77 - expected_character = 123 - - # Test parsing position data - position: Position = Position.from_dict({'line': expected_line, 'character': expected_character}) - assert position.line == expected_line and position.character == expected_character - - # Test round trip conversion of the position data - position_copy = Position.from_dict(position.to_dict()) - assert position == position_copy - assert json.dumps(position.to_dict()) == json.dumps(position_copy.to_dict()) - - -def test_range(): - # Create our expected variables we'll construct tests with. - expected_start = Position(123, 456) - expected_end = Position(789, 100) - - # Test parsing range data - range_item: Range = Range.from_dict({ - 'start': expected_start.to_dict(), - 'end': expected_end.to_dict() - }) - assert range_item.start == expected_start and range_item.end == expected_end - - # Test round trip conversion of the range data - range_copy = Range.from_dict(range_item.to_dict()) - assert range_item == range_copy - assert json.dumps(range_item.to_dict()) == json.dumps(range_copy.to_dict()) - - -def test_location(): - # Create our expected variables we'll construct tests with. - expected_uri = "/file/testpath/testuripath" - expected_range = Range(Position(123, 456), Position(789, 100)) - - # Test parsing location data - location: Location = Location.from_dict({ - 'uri': expected_uri, - 'range': expected_range.to_dict() - }) - assert location.uri == expected_uri and \ - location.range == expected_range - - # Test round trip conversion of the location data - location_copy = Location.from_dict(location.to_dict()) - assert location == location_copy - assert json.dumps(location.to_dict()) == json.dumps(location_copy.to_dict()) - - -def test_location_link(): - # Create our expected variables we'll construct tests with. - expected_origin_selection_range = Range(Position(123, 456), Position(789, 100)) - expected_target_uri = "c:\\file\\origin.uri" - expected_target_range = Range(Position(321, 654), Position(987, 1)) - expected_target_selection_range = Range(Position(132, 465), Position(798, 205)) - - # Test parsing location link data - location_link: LocationLink = LocationLink.from_dict({ - 'originSelectionRange': expected_origin_selection_range.to_dict(), - 'targetUri': expected_target_uri, - 'targetRange': expected_target_range.to_dict(), - 'targetSelectionRange': expected_target_selection_range.to_dict() - }) - assert location_link.origin_selection_range == expected_origin_selection_range and \ - location_link.target_uri == expected_target_uri and \ - location_link.target_range == expected_target_range and \ - location_link.target_selection_range == expected_target_selection_range - - # Test round trip conversion of the location data - location_link_copy = LocationLink.from_dict(location_link.to_dict()) - assert location_link == location_link_copy - assert json.dumps(location_link.to_dict()) == json.dumps(location_link_copy.to_dict()) - - -def test_diagnostic_related_information(): - # Create our expected variables we'll construct tests with. - expected_location = Location("/file/testpath/testuripath", Range(Position(123, 456), Position(789, 100))) - expected_message = "testMessagePlaceholder" - - # Test parsing diagnostic related information - diagnostic_related_info: DiagnosticRelatedInformation = DiagnosticRelatedInformation.from_dict({ - 'location': expected_location.to_dict(), - 'message': expected_message - }) - assert diagnostic_related_info.location == expected_location and diagnostic_related_info.message == expected_message - - # Test round trip conversion of the location data - diagnostic_related_info_copy = DiagnosticRelatedInformation.from_dict(diagnostic_related_info.to_dict()) - assert diagnostic_related_info == diagnostic_related_info_copy - assert json.dumps(diagnostic_related_info.to_dict()) == json.dumps(diagnostic_related_info_copy.to_dict()) - - -def test_code_description(): - # Create our expected variables we'll construct tests with. - expected_href = "testHrefString" - - # Test parsing code description - code_description: CodeDescription = CodeDescription.from_dict({ - 'href': expected_href - }) - assert code_description.href == expected_href - - # Test round trip conversion of the location data - code_description_copy = CodeDescription.from_dict(code_description.to_dict()) - assert code_description == code_description_copy - assert json.dumps(code_description.to_dict()) == json.dumps(code_description_copy.to_dict()) - - -def test_diagnostic(): - # Create our expected variables we'll construct tests with. - expected_range = Range(Position(123, 456), Position(789, 100)) - expected_severity = DiagnosticSeverity.ERROR - expected_code = "X9-01-07-1992" - expected_code_description = CodeDescription("testHrefData") - expected_source = "testSourceData" - expected_message = "testMessageData" - expected_tags = [DiagnosticTag.DEPRECATED, DiagnosticTag.UNNECESSARY] - expected_related_info = [ - DiagnosticRelatedInformation( - Location('locationUri', Range(Position(111, 222), Position(333, 444))), 'diagnosticRelatedInfo' - ), - DiagnosticRelatedInformation( - Location('locationUri2', Range(Position(555, 666), Position(777, 888))), 'diagnosticRelatedInfo2' - ) - ] - expected_data = "testData" - - # Test parsing location link data - diagnostic: Diagnostic = Diagnostic.from_dict({ - 'range': expected_range.to_dict(), - 'severity': int(expected_severity), - 'code': expected_code, - 'codeDescription': expected_code_description.to_dict(), - 'source': expected_source, - 'message': expected_message, - 'tags': [int(diagnostic_tag) for diagnostic_tag in expected_tags], - 'relatedInformation': [related_info.to_dict() for related_info in expected_related_info], - 'data': expected_data - }) - assert diagnostic.range == expected_range and \ - diagnostic.severity == expected_severity and \ - diagnostic.code == expected_code and \ - diagnostic.code_description == expected_code_description and \ - diagnostic.source == expected_source and \ - diagnostic.message == expected_message and \ - diagnostic.tags == expected_tags and \ - diagnostic.related_information == expected_related_info and \ - diagnostic.data == expected_data - - # Test round trip conversion of the location data - diagnostic_copy = Diagnostic.from_dict(diagnostic.to_dict()) - assert diagnostic == diagnostic_copy - assert json.dumps(diagnostic.to_dict()) == json.dumps(diagnostic_copy.to_dict()) - - -def test_command(): - # Create our expected variables we'll construct tests with. - expected_title = "testHrefString" - expected_command = "testCommandString" - expected_arguments = [ - {'A': 'OK', 'B': 'Hello!'}, - 0, - 77, - False - ] - - # Test parsing command data - command: Command = Command.from_dict({ - 'title': expected_title, - 'command': expected_command, - 'arguments': expected_arguments - }) - assert command.title == expected_title and \ - command.command == expected_command and \ - command.arguments == expected_arguments - - # Test round trip conversion of the location data - command_copy = Command.from_dict(command.to_dict()) - assert command == command_copy - assert json.dumps(command.to_dict()) == json.dumps(command_copy.to_dict()) - - -def test_textedit(): - # Create our expected variables we'll construct tests with. - expected_range = Range(Position(123, 456), Position(321, 654)) - expected_new_text = "testNewText" - - # Test parsing location link data - text_edit: TextEdit = TextEdit.from_dict({ - 'range': expected_range.to_dict(), - 'newText': expected_new_text - }) - assert text_edit.range == expected_range and text_edit.new_text == expected_new_text - - # Test round trip conversion of the location data - text_edit_copy = TextEdit.from_dict(text_edit.to_dict()) - assert text_edit == text_edit_copy - assert json.dumps(text_edit.to_dict()) == json.dumps(text_edit_copy.to_dict()) - - -def test_change_annotation(): - # Create our expected variables we'll construct tests with. - expected_label = "testLabel" - expected_needs_confirmation = False - expected_description = "testDescription" - - # Test parsing location link data - change_annotation: ChangeAnnotation = ChangeAnnotation.from_dict({ - 'label': expected_label, - 'needsConfirmation': expected_needs_confirmation, - 'description': expected_description - }) - assert change_annotation.label == expected_label and \ - change_annotation.needs_confirmation == expected_needs_confirmation and \ - change_annotation.description == expected_description - - # Test round trip conversion of the location data - change_annotation_copy = ChangeAnnotation.from_dict(change_annotation.to_dict()) - assert change_annotation == change_annotation_copy - assert json.dumps(change_annotation.to_dict()) == json.dumps(change_annotation_copy.to_dict()) - - -def test_annotated_text_edit(): - # Create our expected variables we'll construct tests with. - expected_range = Range(Position(123, 456), Position(321, 654)) - expected_new_text = "testNewText" - expected_annotation_id = "testAnnotationId" - - # Test parsing location link data - annotated_text_edit: AnnotatedTextEdit = AnnotatedTextEdit.from_dict({ - 'range': expected_range.to_dict(), - 'newText': expected_new_text, - 'annotationId': expected_annotation_id - }) - assert annotated_text_edit.range == expected_range and annotated_text_edit.new_text == expected_new_text - - # Test round trip conversion of the location data - annotated_text_edit_copy = AnnotatedTextEdit.from_dict(annotated_text_edit.to_dict()) - assert annotated_text_edit == annotated_text_edit_copy - assert json.dumps(annotated_text_edit.to_dict()) == json.dumps(annotated_text_edit_copy.to_dict()) \ No newline at end of file diff --git a/tests/test_serializable_structure.py b/tests/test_serializable_structure.py deleted file mode 100644 index 1df769f..0000000 --- a/tests/test_serializable_structure.py +++ /dev/null @@ -1,114 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional, List, Union, Dict - -from slither_lsp.lsp.types.base_serializable_structure import SerializableStructure, serialization_metadata - - -@dataclass -class TestClassA(SerializableStructure): - num: int - - -@dataclass -class TestClassB(TestClassA): - num2: int - -@dataclass -class TestComplexTypeHints(SerializableStructure): - super_union: Union[bool, Union[str, str, bool, Union[TestClassA, None], None], None] - statuses: List[bool] - texts: List[str] - ids: List[Union[str, int]] - commands: Union[str, List[str]] - test: List[List[List[List[str]]]] - bool_with_override: bool = field( - default=False, - metadata=serialization_metadata(name_override="SPECIAL_NAME_BOOL") - ) - test_basic_list: Optional[list] = None - test_basic_list2: list = field(default_factory=list) - excluded_null: Optional[str] = None - included_null: Optional[str] = field(default=None, metadata=serialization_metadata(include_none=True)) - constant_test: str = field(default='CONSTANT_VALUE', metadata=serialization_metadata(enforce_as_constant=True)) - - -# Create a basic structure -testComplexTypeHints = TestComplexTypeHints( - super_union=TestClassA(0), - statuses=[True, True, False, True], - texts=["ok", "OK"], - ids=["id1", 2, "id3", 4], - commands=["cmd", "-c", "echo hi"], - test=[[], [[[]]], [[["hi", "ok"]]]], - test_basic_list=["ok", "ok2", "ok3", 7, [7, 8, 9]], - test_basic_list2=["ok4", "ok5", "ok6", 1, [2, 3, 4]], -) - - -def test_basic_inheritance(): - b = TestClassB(0, 7) - result = b.to_dict() - assert 'num' in result and 'num2' in result - b_copy = TestClassB.from_dict(b.to_dict()) - assert b.num == b_copy.num and b.num2 == b_copy.num2 - - -def test_deserialization(): - # Serialize testComplexTypeHints - serialized = testComplexTypeHints.to_dict() - - # Verify our included/excluded null values are/aren't there, as expected. - assert 'includedNull' in serialized - assert 'excludedNull' not in serialized - - # Verify round trip serialization - b_copy = TestComplexTypeHints.from_dict(serialized) - assert testComplexTypeHints == b_copy - - -def test_name_override(): - # Serialize testComplexTypeHints - serialized = testComplexTypeHints.to_dict() - - # Verify our name override is valid - assert 'SPECIAL_NAME_BOOL' in serialized - - -def test_enforce_as_constant(): - # Serialize testComplexTypeHints - serialized = testComplexTypeHints.to_dict() - - # Set a bad value for the constant in the dictionary and try to deserialize. - failed_bad_constant = False - try: - serialized['constantTest'] = 'BAD_CONSTANT_VALUE' - b_failed_example = TestComplexTypeHints.from_dict(serialized) - except ValueError: - failed_bad_constant = True - assert failed_bad_constant - - -@dataclass -class TestDictStruct(SerializableStructure): - x: int - y: TestClassA - z: Dict[str, TestClassA] - - -def test_struct_with_dict(): - # Create a test structure with a dictionary - test_dict = TestDictStruct( - x=0, - y=TestClassA(7), - z={ - "first": TestClassA(1), - "second": TestClassA(2) - } - ) - - # Serialize our structure - serialized = test_dict.to_dict() - - # Verify round trip serialization - test_dict_copy = TestDictStruct.from_dict(serialized) - assert test_dict == test_dict_copy