Skip to content

Commit b3c5dd3

Browse files
committed
feat(langserver): improve and unify Robocop 6 configuration loading
- Enhance configuration loading process - Add notifications for faulty configurations closes #469
1 parent 8ef14c7 commit b3c5dd3

File tree

6 files changed

+154
-101
lines changed

6 files changed

+154
-101
lines changed

packages/language_server/src/robotcode/language_server/robotframework/parts/formatting.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,22 @@
1717

1818
from ..configuration import RoboCopConfig, RoboTidyConfig
1919
from .protocol_part import RobotLanguageServerProtocolPart
20-
from .robocop_tidy_mixin import RoboCopTidyMixin
2120

2221
if TYPE_CHECKING:
2322
from ..protocol import RobotLanguageServerProtocol
2423

2524

26-
class RobotFormattingProtocolPart(RobotLanguageServerProtocolPart, RoboCopTidyMixin):
25+
class RobotFormattingProtocolPart(RobotLanguageServerProtocolPart):
2726
_logger = LoggingDescriptor()
2827

2928
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
3029
super().__init__(parent)
3130

3231
parent.formatting.format.add(self.format)
3332

34-
if self.robotidy_installed or (self.robocop_installed and self.robocop_version >= (6, 0)):
33+
if self.parent.robocop_helper.robotidy_installed or (
34+
self.parent.robocop_helper.robocop_installed and self.parent.robocop_helper.robocop_version >= (6, 0)
35+
):
3536
parent.formatting.format_range.add(self.format_range)
3637

3738
self.space_count = 4
@@ -64,8 +65,8 @@ def format(
6465
options: FormattingOptions,
6566
**further_options: Any,
6667
) -> Optional[List[TextEdit]]:
67-
if self.robocop_installed and self.robocop_version >= (6, 0):
68-
if not self.is_robocop_notification_shown and self.robotidy_installed:
68+
if self.parent.robocop_helper.robocop_installed and self.parent.robocop_helper.robocop_version >= (6, 0):
69+
if not self.is_robocop_notification_shown and self.parent.robocop_helper.robotidy_installed:
6970
self.parent.window.show_message(
7071
"`robotframework-robocop >= 6.0` is installed and will be used for formatting.\n\n"
7172
"`robotframework-tidy` is also detected in the workspace, but its use is redundant.\n"
@@ -78,7 +79,7 @@ def format(
7879
return self.format_robocop(document, options, **further_options)
7980

8081
tidy_config = self.get_tidy_config(document)
81-
if (tidy_config.enabled or get_robot_version() >= (5, 0)) and self.robotidy_installed:
82+
if (tidy_config.enabled or get_robot_version() >= (5, 0)) and self.parent.robocop_helper.robotidy_installed:
8283
return self.format_robot_tidy(document, options, config=tidy_config, **further_options)
8384

8485
if get_robot_version() < (5, 0):
@@ -105,18 +106,18 @@ def format_robot_tidy(
105106

106107
model = self.parent.documents_cache.get_model(document, False)
107108

108-
if self.robotidy_version >= (3, 0):
109+
if self.parent.robocop_helper.robotidy_version >= (3, 0):
109110
from robotidy.api import get_robotidy
110111
from robotidy.disablers import RegisterDisablers
111112

112-
if self.robotidy_version >= (4, 2):
113+
if self.parent.robocop_helper.robotidy_version >= (4, 2):
113114
robot_tidy = get_robotidy(
114115
document.uri.to_path(),
115116
None,
116117
ignore_git_dir=config.ignore_git_dir,
117118
config=config.config,
118119
)
119-
elif self.robotidy_version >= (4, 1):
120+
elif self.parent.robocop_helper.robotidy_version >= (4, 1):
120121
robot_tidy = get_robotidy(
121122
document.uri.to_path(),
122123
None,
@@ -135,14 +136,14 @@ def format_robot_tidy(
135136
)
136137
disabler_finder.visit(model)
137138

138-
if self.robotidy_version >= (4, 11):
139+
if self.parent.robocop_helper.robotidy_version >= (4, 11):
139140
if disabler_finder.is_disabled_in_file():
140141
return None
141142
else:
142143
if disabler_finder.file_disabled:
143144
return None
144145

145-
if self.robotidy_version >= (4, 0):
146+
if self.parent.robocop_helper.robotidy_version >= (4, 0):
146147
_, _, new, _ = robot_tidy.transform_until_stable(model, disabler_finder)
147148
else:
148149
_, _, new = robot_tidy.transform(model, disabler_finder.disablers)
@@ -156,7 +157,7 @@ def format_robot_tidy(
156157
robot_tidy.formatting_config.start_line = range.start.line + 1
157158
robot_tidy.formatting_config.end_line = range.end.line + 1
158159

159-
if self.robotidy_version >= (2, 2):
160+
if self.parent.robocop_helper.robotidy_version >= (2, 2):
160161
from robotidy.disablers import RegisterDisablers
161162

162163
disabler_finder = RegisterDisablers(
@@ -197,19 +198,13 @@ def format_robocop(
197198
range: Optional[Range] = None,
198199
**further_options: Any,
199200
) -> Optional[List[TextEdit]]:
200-
from robocop.config import ConfigManager
201201
from robocop.formatter.runner import RobocopFormatter
202202

203-
robocop_config = self.get_robocop_config(document)
204203
workspace_folder = self.parent.workspace.get_workspace_folder(document.uri)
204+
if workspace_folder is None:
205+
return None
205206

206-
config_manager = ConfigManager(
207-
[document.uri.to_path()],
208-
root=workspace_folder.uri.to_path() if workspace_folder else None,
209-
config=robocop_config.config_file,
210-
ignore_git_dir=robocop_config.ignore_git_dir,
211-
ignore_file_config=robocop_config.ignore_file_config,
212-
)
207+
config_manager = self.parent.robocop_helper.get_config_manager(workspace_folder)
213208

214209
config = config_manager.get_config_for_source_file(document.uri.to_path())
215210

@@ -284,7 +279,9 @@ def format_range(
284279
**further_options: Any,
285280
) -> Optional[List[TextEdit]]:
286281
config = self.get_tidy_config(document)
287-
if (config.enabled and self.robotidy_installed) or (self.robocop_installed and self.robocop_version >= (6, 0)):
282+
if (config.enabled and self.parent.robocop_helper.robotidy_installed) or (
283+
self.parent.robocop_helper.robocop_installed and self.parent.robocop_helper.robocop_version >= (6, 0)
284+
):
288285
return self.format_robot_tidy(document, options, range=range, config=config, **further_options)
289286

290287
return None

packages/language_server/src/robotcode/language_server/robotframework/parts/project_info.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
from ...__version__ import __version__ as robotcode_version
1212
from .protocol_part import RobotLanguageServerProtocolPart
13-
from .robocop_tidy_mixin import RoboCopTidyMixin
1413

1514
if TYPE_CHECKING:
1615
from ..protocol import RobotLanguageServerProtocol
@@ -26,7 +25,7 @@ class ProjectInfo(CamelSnakeMixin):
2625
robot_code_version_string: Optional[str] = None
2726

2827

29-
class ProjectInfoPart(RobotLanguageServerProtocolPart, RoboCopTidyMixin):
28+
class ProjectInfoPart(RobotLanguageServerProtocolPart):
3029
_logger = LoggingDescriptor()
3130

3231
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -40,12 +39,12 @@ def _robot_project_info(
4039
**kwargs: Any,
4140
) -> ProjectInfo:
4241
robocop_version_string = None
43-
if self.robocop_installed:
44-
robocop_version_string = self.robocop_version_str
42+
if self.parent.robocop_helper.robocop_installed:
43+
robocop_version_string = self.parent.robocop_helper.robocop_version_str
4544

4645
tidy_version_string = None
47-
if self.robotidy_installed:
48-
tidy_version_string = self.robotidy_version_str
46+
if self.parent.robocop_helper.robotidy_installed:
47+
tidy_version_string = self.parent.robocop_helper.robotidy_version_str
4948

5049
return ProjectInfo(
5150
robot_version_string=get_version(),

packages/language_server/src/robotcode/language_server/robotframework/parts/robocop_diagnostics.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,14 @@
1818
from ...common.parts.diagnostics import DiagnosticsCollectType, DiagnosticsResult
1919
from ..configuration import RoboCopConfig
2020
from .protocol_part import RobotLanguageServerProtocolPart
21-
from .robocop_tidy_mixin import RoboCopTidyMixin
2221

2322
if TYPE_CHECKING:
2423
from robocop.linter.runner import RobocopLinter
2524

2625
from ..protocol import RobotLanguageServerProtocol
2726

2827

29-
class RobotRoboCopDiagnosticsProtocolPart(RobotLanguageServerProtocolPart, RoboCopTidyMixin):
28+
class RobotRoboCopDiagnosticsProtocolPart(RobotLanguageServerProtocolPart):
3029
_logger = LoggingDescriptor()
3130

3231
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -35,7 +34,7 @@ def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
3534
self.source_name = "robocop"
3635
self._robocop_linters: WeakKeyDictionary[WorkspaceFolder, "RobocopLinter"] = WeakKeyDictionary()
3736

38-
if self.robocop_installed:
37+
if self.parent.robocop_helper.robocop_installed:
3938
parent.diagnostics.collect.add(self.collect_diagnostics)
4039

4140
def get_config(self, document: TextDocument) -> Optional[RoboCopConfig]:
@@ -50,18 +49,15 @@ def get_config(self, document: TextDocument) -> Optional[RoboCopConfig]:
5049
def collect_diagnostics(
5150
self, sender: Any, document: TextDocument, diagnostics_type: DiagnosticsCollectType
5251
) -> Optional[DiagnosticsResult]:
53-
if self.robocop_installed:
52+
if self.parent.robocop_helper.robocop_installed:
5453
workspace_folder = self.parent.workspace.get_workspace_folder(document.uri)
5554
if workspace_folder is not None:
5655
config = self.get_config(document)
5756

5857
if config is not None and config.enabled:
59-
if self.robocop_version >= (6, 0):
58+
if self.parent.robocop_helper.robocop_version >= (6, 0):
6059
# In Robocop 6.0, the diagnostics are collected in a different way
61-
return DiagnosticsResult(
62-
self.collect_diagnostics,
63-
self.collect(document, workspace_folder, config),
64-
)
60+
return DiagnosticsResult(self.collect_diagnostics, self.collect(document, workspace_folder))
6561

6662
return DiagnosticsResult(
6763
self.collect_diagnostics,
@@ -71,26 +67,17 @@ def collect_diagnostics(
7167
return None
7268

7369
@_logger.call
74-
def collect(
75-
self,
76-
document: TextDocument,
77-
workspace_folder: WorkspaceFolder,
78-
extension_config: RoboCopConfig,
79-
) -> List[Diagnostic]:
80-
from robocop.config import ConfigManager
70+
def collect(self, document: TextDocument, workspace_folder: WorkspaceFolder) -> List[Diagnostic]:
8171
from robocop.linter.rules import RuleSeverity
8272
from robocop.linter.runner import RobocopLinter
8373

8474
linter = self._robocop_linters.get(workspace_folder, None)
8575

8676
if linter is None:
87-
config_manager = ConfigManager(
88-
[],
89-
root=workspace_folder.uri.to_path(),
90-
config=extension_config.config_file,
91-
ignore_git_dir=extension_config.ignore_git_dir,
92-
ignore_file_config=extension_config.ignore_file_config,
93-
)
77+
config_manager = self.parent.robocop_helper.get_config_manager(workspace_folder)
78+
79+
config = config_manager.get_config_for_source_file(document.uri.to_path())
80+
9481
linter = RobocopLinter(config_manager)
9582
self._robocop_linters[workspace_folder] = linter
9683

@@ -128,7 +115,7 @@ def collect(
128115
),
129116
source=self.source_name,
130117
code=f"{diagnostic.rule.rule_id}-{diagnostic.rule.name}",
131-
code_description=self.get_code_description(self.robocop_version, diagnostic),
118+
code_description=self.get_code_description(self.parent.robocop_helper.robocop_version, diagnostic),
132119
)
133120
for diagnostic in diagnostics
134121
]
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import functools
2+
from pathlib import Path
3+
from typing import TYPE_CHECKING, Dict, Union
4+
5+
from robotcode.core.lsp.types import MessageType
6+
from robotcode.core.text_document import TextDocument
7+
from robotcode.core.utils.logging import LoggingDescriptor
8+
from robotcode.core.utils.version import Version, create_version_from_str
9+
from robotcode.core.workspace import WorkspaceFolder
10+
11+
from ..configuration import RoboCopConfig
12+
from .protocol_part import RobotLanguageServerProtocolPart
13+
14+
if TYPE_CHECKING:
15+
from ..protocol import RobotLanguageServerProtocol
16+
17+
if TYPE_CHECKING:
18+
from robocop.config import ConfigManager
19+
20+
21+
class RobocopConfigError(Exception):
22+
"""Robocop configuration errors."""
23+
24+
25+
class RoboCopHelper(RobotLanguageServerProtocolPart):
26+
_logger = LoggingDescriptor()
27+
28+
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
29+
super().__init__(parent)
30+
self._config_managers: Dict[WorkspaceFolder, "ConfigManager"] = {}
31+
32+
@functools.cached_property
33+
def robotidy_installed(self) -> bool:
34+
try:
35+
__import__("robotidy")
36+
except ImportError:
37+
return False
38+
return True
39+
40+
@functools.cached_property
41+
def robotidy_version(self) -> Version:
42+
from robotidy.version import __version__
43+
44+
return create_version_from_str(__version__)
45+
46+
@functools.cached_property
47+
def robotidy_version_str(self) -> str:
48+
from robotidy.version import __version__
49+
50+
return str(__version__)
51+
52+
@functools.cached_property
53+
def robocop_installed(self) -> bool:
54+
try:
55+
__import__("robocop")
56+
except ImportError:
57+
return False
58+
return True
59+
60+
@functools.cached_property
61+
def robocop_version(self) -> Version:
62+
from robocop import __version__
63+
64+
return create_version_from_str(__version__)
65+
66+
@functools.cached_property
67+
def robocop_version_str(self) -> str:
68+
from robocop import __version__
69+
70+
return str(__version__)
71+
72+
def get_robocop_config(self, resource: Union[TextDocument, WorkspaceFolder]) -> RoboCopConfig:
73+
folder = (
74+
self.parent.workspace.get_workspace_folder(resource.uri) if isinstance(resource, TextDocument) else resource
75+
)
76+
if folder is None:
77+
return RoboCopConfig()
78+
79+
return self.parent.workspace.get_configuration(RoboCopConfig, folder.uri)
80+
81+
def get_config_manager(self, workspace_folder: WorkspaceFolder) -> "ConfigManager":
82+
from robocop.config import ConfigManager
83+
84+
if workspace_folder in self._config_managers:
85+
return self._config_managers[workspace_folder]
86+
87+
config = self.get_robocop_config(workspace_folder)
88+
89+
result = None
90+
try:
91+
config_path = None
92+
93+
if config.config_file:
94+
config_path = Path(config.config_file)
95+
if not config_path.exists():
96+
raise RobocopConfigError(f"Config file {config_path} does not exist.")
97+
98+
result = ConfigManager(
99+
[],
100+
root=workspace_folder.uri.to_path(),
101+
config=config_path,
102+
ignore_git_dir=config.ignore_git_dir,
103+
ignore_file_config=config.ignore_file_config,
104+
)
105+
self._config_managers[workspace_folder] = result
106+
return result
107+
except Exception as e:
108+
self._logger.exception(e)
109+
e_msg = str(e)
110+
error_details = f": {e_msg}" if e_msg else ""
111+
self.parent.window.show_message(
112+
f"Robocop configuration could not be loaded{error_details}. "
113+
f"Please verify your configuration files "
114+
f"and workspace settings. Check the output logs for detailed error information.",
115+
MessageType.ERROR,
116+
)
117+
raise RobocopConfigError(f"Failed to load Robocop configuration: {e} ({e.__class__.__qualname__})") from e

0 commit comments

Comments
 (0)