Skip to content

Commit 310bc54

Browse files
committed
feat(langserver): implemented robocop 6.0 formatting and deprecate old robotidy
1 parent 37c5371 commit 310bc54

File tree

9 files changed

+176
-77
lines changed

9 files changed

+176
-77
lines changed

package.json

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,13 +1116,13 @@
11161116
}
11171117
},
11181118
{
1119-
"title": "Linting - Robocop",
1119+
"title": "Linting and Formatting - Robocop",
11201120
"type": "object",
11211121
"properties": {
11221122
"robotcode.robocop.enabled": {
11231123
"type": "boolean",
11241124
"default": true,
1125-
"markdownDescription": "Enables 'robocop' code analysis, if installed. See [robocop](https://github.com/MarketSquare/robotframework-robocop)",
1125+
"markdownDescription": "Enables 'robocop' code analysis, if installed. See [robocop](https://robocop.readthedocs.io/)",
11261126
"scope": "resource"
11271127
},
11281128
"robotcode.robocop.include": {
@@ -1132,15 +1132,35 @@
11321132
"type": "string"
11331133
},
11341134
"description": "Include specified 'robocop' rules. You can define rule by its name or id. Glob patterns are supported",
1135-
"scope": "resource"
1135+
"scope": "resource",
1136+
"markdownDeprecationMessage": "This is a setting for an old `robotframework-robocop` version `<6.0` and will be removed in the future"
11361137
},
11371138
"robotcode.robocop.exclude": {
11381139
"type": "array",
11391140
"default": [],
11401141
"items": {
11411142
"type": "string"
11421143
},
1143-
"description": "Exlude specified 'robocop' rules. You can define rule by its name or id. Glob patterns are supported",
1144+
"description": "Exclude specified 'robocop' rules. You can define rule by its name or id. Glob patterns are supported",
1145+
"scope": "resource",
1146+
"markdownDeprecationMessage": "This is a setting for an old `robotframework-robocop` version `<6.0` and will be removed in the future."
1147+
},
1148+
"robotcode.robocop.ignoreGitDir": {
1149+
"type": "boolean",
1150+
"default": false,
1151+
"markdownDescription": "Do not stop searching for config file when .git directory is found. Corresponds to the `--ignore-git-dir` of _robocop_ See [robocop](https://robocop.readthedocs.io/)",
1152+
"scope": "resource"
1153+
},
1154+
"robotcode.robocop.configFile": {
1155+
"type": "string",
1156+
"default": null,
1157+
"markdownDescription": "Read configuration from FILE path. Corresponds to the `--config` option of _robocop_ See [robocop](https://robocop.readthedocs.io/)",
1158+
"scope": "resource"
1159+
},
1160+
"robotcode.robocop.ignoreFileConfig": {
1161+
"type": "boolean",
1162+
"default": false,
1163+
"markdownDescription": "Do not load configuration files. Corresponds to the `--ignore-file-config` option of _robocop_ See [robocop](https://robocop.readthedocs.io/)",
11441164
"scope": "resource"
11451165
},
11461166
"robotcode.robocop.configurations": {
@@ -1149,8 +1169,9 @@
11491169
"items": {
11501170
"type": "string"
11511171
},
1152-
"description": "Configure 'robocop' checker with parameter value.",
1153-
"scope": "resource"
1172+
"description": "Configure checker or report with parameter value. Corresponds to the `--configure` option of _robocop_ See [robocop](https://robocop.readthedocs.io/)",
1173+
"scope": "resource",
1174+
"markdownDeprecationMessage": "This is a setting for an old `robotframework-robocop` version `<6.0` and will be removed in the future."
11541175
}
11551176
}
11561177
},

packages/core/src/robotcode/core/utils/version.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import functools
12
import re
23
from typing import NamedTuple, Optional
34

@@ -16,6 +17,7 @@ class Version(NamedTuple):
1617
dev: Optional[int] = None
1718

1819

20+
@functools.lru_cache(maxsize=128)
1921
def create_version_from_str(version_str: str) -> Version:
2022
def s_to_i(s: Optional[str]) -> Optional[int]:
2123
return int(s) if s is not None else None

packages/language_server/src/robotcode/language_server/robotframework/configuration.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class RoboCopConfig(ConfigBase):
3333
enabled: bool = True
3434
include: List[str] = field(default_factory=list)
3535
exclude: List[str] = field(default_factory=list)
36+
ignore_git_dir: bool = False
37+
ignore_file_config: bool = False
38+
config_file: Optional[str] = None
3639
configurations: List[str] = field(default_factory=list)
3740

3841

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

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import io
22
import os
3-
import re
43
from concurrent.futures import CancelledError
54
from typing import TYPE_CHECKING, Any, List, Optional, cast
65

@@ -14,49 +13,48 @@
1413
)
1514
from robotcode.core.text_document import TextDocument
1615
from robotcode.core.utils.logging import LoggingDescriptor
17-
from robotcode.core.utils.version import create_version_from_str
18-
from robotcode.robot.diagnostics.model_helper import ModelHelper
1916
from robotcode.robot.utils import get_robot_version
2017

21-
from ..configuration import RoboTidyConfig
18+
from ..configuration import RoboCopConfig, RoboTidyConfig
2219
from .protocol_part import RobotLanguageServerProtocolPart
20+
from .robocop_tidy_mixin import RoboCopTidyMixin
2321

2422
if TYPE_CHECKING:
2523
from ..protocol import RobotLanguageServerProtocol
2624

2725

28-
def robotidy_installed() -> bool:
29-
try:
30-
__import__("robotidy")
31-
except ImportError:
32-
return False
33-
return True
34-
35-
36-
class RobotFormattingProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
26+
class RobotFormattingProtocolPart(RobotLanguageServerProtocolPart, RoboCopTidyMixin):
3727
_logger = LoggingDescriptor()
3828

3929
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
4030
super().__init__(parent)
4131

4232
parent.formatting.format.add(self.format)
4333

44-
if robotidy_installed():
34+
if self.robotidy_installed or (self.robocop_installed and self.robocop_version >= (6, 0)):
4535
parent.formatting.format_range.add(self.format_range)
4636

4737
self.space_count = 4
4838
self.use_pipes = False
4939
self.line_separator = os.linesep
5040
self.short_test_name_length = 18
5141
self.setting_and_variable_name_length = 14
42+
self.robocop_installed_message_shown = False
5243

53-
def get_config(self, document: TextDocument) -> RoboTidyConfig:
44+
def get_tidy_config(self, document: TextDocument) -> RoboTidyConfig:
5445
folder = self.parent.workspace.get_workspace_folder(document.uri)
5546
if folder is None:
5647
return RoboTidyConfig()
5748

5849
return self.parent.workspace.get_configuration(RoboTidyConfig, folder.uri)
5950

51+
def get_robocop_config(self, document: TextDocument) -> RoboCopConfig:
52+
folder = self.parent.workspace.get_workspace_folder(document.uri)
53+
if folder is None:
54+
return RoboCopConfig()
55+
56+
return self.parent.workspace.get_configuration(RoboCopConfig, folder.uri)
57+
6058
@language_id("robotframework")
6159
@_logger.call
6260
def format(
@@ -66,23 +64,32 @@ def format(
6664
options: FormattingOptions,
6765
**further_options: Any,
6866
) -> Optional[List[TextEdit]]:
69-
config = self.get_config(document)
67+
if (get_robot_version() >= (5, 0)) and self.robocop_installed and self.robocop_version >= (6, 0):
68+
if not self.robocop_installed_message_shown and self.robotidy_installed:
69+
self.parent.window.show_message(
70+
"`robotframework-robocop >= 6.0` is installed and will be used for formatting.\n"
71+
"`robotframework-tidy` is also detected in the workspace. "
72+
"It is not needed as `robocop` handles formatting tasks.\n",
73+
MessageType.INFO,
74+
)
75+
self.robocop_installed_message_shown = True
7076

71-
if (config.enabled or get_robot_version() >= (5, 0)) and robotidy_installed():
72-
return self.format_robot_tidy(document, options, config=config, **further_options)
77+
return self.format_robocop(document, options, **further_options)
78+
79+
tidy_config = self.get_tidy_config(document)
80+
if (tidy_config.enabled or get_robot_version() >= (5, 0)) and self.robotidy_installed:
81+
return self.format_robot_tidy(document, options, config=tidy_config, **further_options)
7382

7483
if get_robot_version() < (5, 0):
7584
return self.format_internal(document, options, **further_options)
7685

7786
self.parent.window.show_message(
78-
"RobotFramework formatter is not available, please install 'robotframework-tidy'.",
87+
"RobotFramework formatter is not available, please install 'robotframework-robocop'.",
7988
MessageType.ERROR,
8089
)
8190

8291
return None
8392

84-
RE_LINEBREAKS = re.compile(r"\r\n|\r|\n")
85-
8693
def format_robot_tidy(
8794
self,
8895
document: TextDocument,
@@ -91,28 +98,24 @@ def format_robot_tidy(
9198
config: Optional[RoboTidyConfig] = None,
9299
**further_options: Any,
93100
) -> Optional[List[TextEdit]]:
94-
from robotidy.version import __version__
95-
96101
try:
97102
if config is None:
98-
config = self.get_config(document)
99-
100-
robotidy_version = create_version_from_str(__version__)
103+
config = self.get_tidy_config(document)
101104

102105
model = self.parent.documents_cache.get_model(document, False)
103106

104-
if robotidy_version >= (3, 0):
107+
if self.robotidy_version >= (3, 0):
105108
from robotidy.api import get_robotidy
106109
from robotidy.disablers import RegisterDisablers
107110

108-
if robotidy_version >= (4, 2):
111+
if self.robotidy_version >= (4, 2):
109112
robot_tidy = get_robotidy(
110113
document.uri.to_path(),
111114
None,
112115
ignore_git_dir=config.ignore_git_dir,
113116
config=config.config,
114117
)
115-
elif robotidy_version >= (4, 1):
118+
elif self.robotidy_version >= (4, 1):
116119
robot_tidy = get_robotidy(
117120
document.uri.to_path(),
118121
None,
@@ -131,14 +134,14 @@ def format_robot_tidy(
131134
)
132135
disabler_finder.visit(model)
133136

134-
if robotidy_version >= (4, 11):
137+
if self.robotidy_version >= (4, 11):
135138
if disabler_finder.is_disabled_in_file():
136139
return None
137140
else:
138141
if disabler_finder.file_disabled:
139142
return None
140143

141-
if robotidy_version >= (4, 0):
144+
if self.robotidy_version >= (4, 0):
142145
_, _, new, _ = robot_tidy.transform_until_stable(model, disabler_finder)
143146
else:
144147
_, _, new = robot_tidy.transform(model, disabler_finder.disablers)
@@ -152,7 +155,7 @@ def format_robot_tidy(
152155
robot_tidy.formatting_config.start_line = range.start.line + 1
153156
robot_tidy.formatting_config.end_line = range.end.line + 1
154157

155-
if robotidy_version >= (2, 2):
158+
if self.robotidy_version >= (2, 2):
156159
from robotidy.disablers import RegisterDisablers
157160

158161
disabler_finder = RegisterDisablers(
@@ -186,6 +189,50 @@ def format_robot_tidy(
186189
self.parent.window.show_message(f"Executing `robotidy` failed: {e}", MessageType.ERROR)
187190
return None
188191

192+
def format_robocop(
193+
self,
194+
document: TextDocument,
195+
options: FormattingOptions,
196+
range: Optional[Range] = None,
197+
**further_options: Any,
198+
) -> Optional[List[TextEdit]]:
199+
from robocop.config import ConfigManager
200+
from robocop.formatter.runner import RobocopFormatter
201+
202+
robocop_config = self.get_robocop_config(document)
203+
204+
config_manager = ConfigManager(
205+
[document.uri.to_path()],
206+
config=robocop_config.config_file,
207+
ignore_git_dir=robocop_config.ignore_git_dir,
208+
ignore_file_config=robocop_config.ignore_file_config,
209+
)
210+
211+
config = config_manager.get_config_for_source_file(document.uri.to_path())
212+
213+
if range is not None:
214+
config.formatter.start_line = range.start.line + 1
215+
config.formatter.end_line = range.end.line + 1
216+
217+
runner = RobocopFormatter(config_manager)
218+
runner.config = config
219+
220+
model = self.parent.documents_cache.get_model(document, False)
221+
_, _, new, _ = runner.format_until_stable(model)
222+
223+
if new.text == document.text():
224+
return None
225+
226+
return [
227+
TextEdit(
228+
range=Range(
229+
start=Position(line=0, character=0),
230+
end=Position(line=len(document.get_lines()), character=0),
231+
),
232+
new_text=new.text,
233+
)
234+
]
235+
189236
def format_internal(
190237
self,
191238
document: TextDocument,
@@ -233,8 +280,8 @@ def format_range(
233280
options: FormattingOptions,
234281
**further_options: Any,
235282
) -> Optional[List[TextEdit]]:
236-
config = self.get_config(document)
237-
if config.enabled and robotidy_installed():
283+
config = self.get_tidy_config(document)
284+
if (config.enabled and self.robotidy_installed) or (self.robocop_installed and self.robocop_version >= (6, 0)):
238285
return self.format_robot_tidy(document, options, range=range, config=config, **further_options)
239286

240287
return None

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

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
from robotcode.core.utils.logging import LoggingDescriptor
88
from robotcode.jsonrpc2.protocol import rpc_method
99

10-
from .formatting import robotidy_installed
1110
from .protocol_part import RobotLanguageServerProtocolPart
12-
from .robocop_diagnostics import robocop_installed
11+
from .robocop_tidy_mixin import RoboCopTidyMixin
1312

1413
if TYPE_CHECKING:
1514
from ..protocol import RobotLanguageServerProtocol
@@ -22,7 +21,7 @@ class ProjectInfo(CamelSnakeMixin):
2221
tidy_version_string: Optional[str] = None
2322

2423

25-
class ProjectInfoPart(RobotLanguageServerProtocolPart):
24+
class ProjectInfoPart(RobotLanguageServerProtocolPart, RoboCopTidyMixin):
2625
_logger = LoggingDescriptor()
2726

2827
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -36,24 +35,12 @@ def _robot_project_info(
3635
**kwargs: Any,
3736
) -> ProjectInfo:
3837
robocop_version_string = None
39-
if robocop_installed():
40-
try:
41-
from robocop.version import __version__
42-
43-
robocop_version_string = __version__
44-
except ImportError:
45-
try:
46-
from robocop import __version__
47-
48-
robocop_version_string = __version__
49-
except ImportError:
50-
pass
38+
if self.robocop_installed:
39+
robocop_version_string = self.robocop_version_str
5140

5241
tidy_version_string = None
53-
if robotidy_installed():
54-
from robotidy.version import __version__
55-
56-
tidy_version_string = __version__
42+
if self.robotidy_installed:
43+
tidy_version_string = self.robotidy_version_str
5744

5845
return ProjectInfo(
5946
robot_version_string=get_version(),

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,21 @@
1717
from ...common.parts.diagnostics import DiagnosticsCollectType, DiagnosticsResult
1818
from ..configuration import RoboCopConfig
1919
from .protocol_part import RobotLanguageServerProtocolPart
20+
from .robocop_tidy_mixin import RoboCopTidyMixin
2021

2122
if TYPE_CHECKING:
2223
from ..protocol import RobotLanguageServerProtocol
2324

2425

25-
def robocop_installed() -> bool:
26-
try:
27-
__import__("robocop")
28-
except ImportError:
29-
return False
30-
return True
31-
32-
33-
class RobotRoboCopDiagnosticsProtocolPart(RobotLanguageServerProtocolPart):
26+
class RobotRoboCopDiagnosticsProtocolPart(RobotLanguageServerProtocolPart, RoboCopTidyMixin):
3427
_logger = LoggingDescriptor()
3528

3629
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
3730
super().__init__(parent)
3831

3932
self.source_name = "robocop"
4033

41-
if robocop_installed():
34+
if self.robocop_installed and self.robocop_version < (6, 0):
4235
parent.diagnostics.collect.add(self.collect_diagnostics)
4336

4437
def get_config(self, document: TextDocument) -> Optional[RoboCopConfig]:

0 commit comments

Comments
 (0)