Skip to content

Commit 0c0198a

Browse files
committed
fix(langserver): normalize path handling should not fully resolve symlinks
fixes #219
1 parent 9332099 commit 0c0198a

File tree

7 files changed

+104
-33
lines changed

7 files changed

+104
-33
lines changed

packages/core/src/robotcode/core/uri.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from typing import Any, Iterator, Mapping, Optional, Union, overload
88
from urllib import parse
99

10+
from .utils.path import normalized_path
11+
1012
_IS_WIN = os.name == "nt"
1113

1214
_RE_DRIVE_LETTER_PATH = re.compile(r"^\/[a-zA-Z]:")
@@ -158,6 +160,10 @@ def params(self) -> str:
158160
def query(self) -> str:
159161
return self._parts.query
160162

163+
@property
164+
def fragment(self) -> str:
165+
return self._parts.fragment
166+
161167
@staticmethod
162168
def from_path(path: Union[str, Path, os.PathLike[str]]) -> Uri:
163169
result = Uri(Path(path).as_uri())
@@ -188,6 +194,26 @@ def __iter__(self) -> Iterator[str]:
188194

189195
def normalized(self) -> Uri:
190196
if self.scheme == "file":
191-
return Uri.from_path(self.to_path().resolve())
197+
return Uri.from_path(normalized_path(self.to_path()))
192198

193199
return Uri(str(self))
200+
201+
def change(
202+
self,
203+
*,
204+
scheme: Optional[str] = None,
205+
netloc: Optional[str] = None,
206+
path: Optional[str] = None,
207+
params: Optional[str] = None,
208+
query: Optional[str] = None,
209+
fragment: Optional[str] = None,
210+
) -> Uri:
211+
212+
return Uri(
213+
scheme=scheme if scheme is not None else self.scheme,
214+
netloc=netloc if netloc is not None else self.netloc,
215+
path=path if path is not None else self.path,
216+
params=params if params is not None else self.params,
217+
query=query if query is not None else self.query,
218+
fragment=fragment if fragment is not None else self.fragment,
219+
)
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,49 @@
1-
from __future__ import annotations
2-
3-
from os import PathLike
1+
import os
2+
import re
43
from pathlib import Path
54
from typing import Any, Union
65

76

87
def path_is_relative_to(
9-
path: Union[Path, str, PathLike[Any]],
10-
other_path: Union[Path, str, PathLike[Any]],
8+
path: Union[Path, str, "os.PathLike[Any]"],
9+
other_path: Union[Path, str, "os.PathLike[Any]"],
1110
) -> bool:
1211
try:
1312
Path(path).relative_to(other_path)
1413
return True
1514
except ValueError:
1615
return False
16+
17+
18+
_RE_DRIVE_LETTER_PATH = re.compile(r"^[a-zA-Z]:")
19+
20+
21+
def normalized_path(path: Union[Path, str, "os.PathLike[Any]"]) -> Path:
22+
p = os.path.normpath(os.path.abspath(path))
23+
24+
if os.name == "nt" and _RE_DRIVE_LETTER_PATH.match(str(p)):
25+
return Path(p[0].upper() + p[1:])
26+
27+
return Path(p)
28+
29+
30+
def normalized_path_full(path: Union[Path, str, "os.PathLike[Any]"]) -> Path:
31+
p = normalized_path(path)
32+
33+
orig_parents = list(reversed(p.parents))
34+
orig_parents.append(p)
35+
36+
parents = []
37+
for index, parent in enumerate(orig_parents):
38+
if parent.exists():
39+
ps = (
40+
next((f.name for f in parent.parent.iterdir() if f.samefile(parent)), None)
41+
if parent.parent is not None and parent.parent != parent
42+
else parent.name or parent.anchor
43+
)
44+
45+
parents.append(ps if ps is not None else parent.name)
46+
else:
47+
return Path(*parents, *[f.name for f in orig_parents[index:]])
48+
49+
return Path(*parents)

packages/debugger/src/robotcode/debugger/listeners.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from robot import result, running
77
from robot.model import Message
88

9+
from robotcode.core.utils.path import normalized_path
10+
911
from .dap_types import Event, Model
1012
from .debugger import Debugger
1113

@@ -18,6 +20,14 @@ class RobotExecutionEventBody(Model):
1820
failed_keywords: Optional[List[Dict[str, Any]]] = None
1921

2022

23+
def source_from_attributes(attributes: Dict[str, Any]) -> str:
24+
s = attributes.get("source", "")
25+
if s:
26+
return str(normalized_path(Path(s)))
27+
28+
return s or ""
29+
30+
2131
class ListenerV2:
2232
ROBOT_LISTENER_API_VERSION = "2"
2333

@@ -32,7 +42,7 @@ def start_suite(self, name: str, attributes: Dict[str, Any]) -> None:
3242
event="robotStarted",
3343
body=RobotExecutionEventBody(
3444
type="suite",
35-
id=f"{attributes.get('source', '')};{attributes.get('longname', '')}",
45+
id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')}",
3646
attributes=dict(attributes),
3747
),
3848
),
@@ -54,7 +64,7 @@ def end_suite(self, name: str, attributes: Dict[str, Any]) -> None:
5464
body=RobotExecutionEventBody(
5565
type="suite",
5666
attributes=dict(attributes),
57-
id=f"{attributes.get('source', '')};{attributes.get('longname', '')}",
67+
id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')}",
5868
failed_keywords=self.failed_keywords,
5969
),
6070
),
@@ -71,7 +81,7 @@ def start_test(self, name: str, attributes: Dict[str, Any]) -> None:
7181
event="robotStarted",
7282
body=RobotExecutionEventBody(
7383
type="test",
74-
id=f"{attributes.get('source', '')};{attributes.get('longname', '')};"
84+
id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')};"
7585
f"{attributes.get('lineno', 0)}",
7686
attributes=dict(attributes),
7787
),
@@ -93,7 +103,7 @@ def end_test(self, name: str, attributes: Dict[str, Any]) -> None:
93103
event="robotEnded",
94104
body=RobotExecutionEventBody(
95105
type="test",
96-
id=f"{attributes.get('source', '')};{attributes.get('longname', '')};"
106+
id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')};"
97107
f"{attributes.get('lineno', 0)}",
98108
attributes=dict(attributes),
99109
failed_keywords=self.failed_keywords,
@@ -142,9 +152,9 @@ def log_message(self, message: Dict[str, Any]) -> None:
142152
item_id = next(
143153
(
144154
(
145-
f"{Path(item.source).resolve() if item.source is not None else ''};{item.longname}"
155+
f"{normalized_path(Path(item.source)) if item.source is not None else ''};{item.longname}"
146156
if item.type == "SUITE"
147-
else f"{Path(item.source).resolve() if item.source is not None else ''};"
157+
else f"{normalized_path(Path(item.source)) if item.source is not None else ''};"
148158
f"{item.longname};{item.line}"
149159
)
150160
for item in Debugger.instance().full_stack_frames
@@ -189,9 +199,9 @@ def message(self, message: Dict[str, Any]) -> None:
189199
item_id = next(
190200
(
191201
(
192-
f"{Path(item.source).resolve() if item.source is not None else ''};{item.longname}"
202+
f"{normalized_path(Path(item.source)) if item.source is not None else ''};{item.longname}"
193203
if item.type == "SUITE"
194-
else f"{Path(item.source).resolve() if item.source is not None else ''};"
204+
else f"{normalized_path(Path(item.source)) if item.source is not None else ''};"
195205
f"{item.longname};{item.line}"
196206
)
197207
for item in Debugger.instance().full_stack_frames
@@ -266,15 +276,15 @@ def enqueue(
266276
item: Union[running.TestSuite, running.TestCase],
267277
) -> Iterator[str]:
268278
if isinstance(item, running.TestSuite):
269-
yield f"{Path(item.source).resolve() if item.source is not None else ''};{item.longname}"
279+
yield f"{normalized_path(item.source) if item.source is not None else ''};{item.longname}"
270280

271281
for s in item.suites:
272282
yield from enqueue(s)
273283
for s in item.tests:
274284
yield from enqueue(s)
275285
return
276286

277-
yield f"{Path(item.source).resolve() if item.source is not None else ''};{item.longname};{item.lineno}"
287+
yield (f"{normalized_path(item.source) if item.source is not None else ''};{item.longname};{item.lineno}")
278288

279289
if self._event_sended:
280290
return

packages/debugger/src/robotcode/debugger/run.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,10 @@ async def run_debugger(
208208
wait_for_debugpy_connected()
209209

210210
args = [
211-
"--listener",
212-
"robotcode.debugger.listeners.ListenerV2",
213211
"--listener",
214212
"robotcode.debugger.listeners.ListenerV3",
213+
"--listener",
214+
"robotcode.debugger.listeners.ListenerV2",
215215
*args,
216216
]
217217

packages/robot/src/robotcode/robot/diagnostics/imports_manager.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from robotcode.core.utils.dataclasses import as_json, from_json
3838
from robotcode.core.utils.glob_path import Pattern, iter_files
3939
from robotcode.core.utils.logging import LoggingDescriptor
40-
from robotcode.core.utils.path import path_is_relative_to
40+
from robotcode.core.utils.path import normalized_path, path_is_relative_to
4141

4242
from ..__version__ import __version__
4343
from ..utils import get_robot_version, get_robot_version_str
@@ -183,7 +183,7 @@ def check_file_changed(self, changes: List[FileEvent]) -> Optional[FileChangeTyp
183183
self._lib_doc.module_spec is not None
184184
and self._lib_doc.module_spec.submodule_search_locations is not None
185185
and any(
186-
path_is_relative_to(path, Path(e).resolve())
186+
path_is_relative_to(path, normalized_path(Path(e)))
187187
for e in self._lib_doc.module_spec.submodule_search_locations
188188
)
189189
)
@@ -197,7 +197,7 @@ def check_file_changed(self, changes: List[FileEvent]) -> Optional[FileChangeTyp
197197
self._lib_doc.module_spec is None
198198
and not self._lib_doc.source
199199
and self._lib_doc.python_path
200-
and any(path_is_relative_to(path, Path(e).resolve()) for e in self._lib_doc.python_path)
200+
and any(path_is_relative_to(path, normalized_path(Path(e))) for e in self._lib_doc.python_path)
201201
)
202202
):
203203
self._invalidate()
@@ -221,14 +221,14 @@ def _update(self) -> None:
221221
self.parent.file_watcher_manager.add_file_watchers(
222222
self.parent.did_change_watched_files,
223223
[
224-
str(Path(location).resolve().joinpath("**"))
224+
str(normalized_path(Path(location)).joinpath("**"))
225225
for location in self._lib_doc.module_spec.submodule_search_locations
226226
],
227227
)
228228
)
229229

230230
if source_or_origin is not None and Path(source_or_origin).parent in [
231-
Path(loc).resolve() for loc in self._lib_doc.module_spec.submodule_search_locations
231+
normalized_path(Path(loc)) for loc in self._lib_doc.module_spec.submodule_search_locations
232232
]:
233233
return
234234

@@ -307,7 +307,7 @@ def check_file_changed(self, changes: List[FileEvent]) -> Optional[FileChangeTyp
307307
path = uri.to_path()
308308
if (
309309
self._document is not None
310-
and (path.resolve() == self._document.uri.to_path().resolve())
310+
and (normalized_path(path) == normalized_path(self._document.uri.to_path()))
311311
or self._document is None
312312
):
313313
self._invalidate()
@@ -1457,7 +1457,7 @@ def _get_entry_for_resource_import(
14571457
def _get_document() -> TextDocument:
14581458
self._logger.debug(lambda: f"Load resource {name} from source {source}")
14591459

1460-
source_path = Path(source).resolve()
1460+
source_path = normalized_path(Path(source))
14611461
extension = source_path.suffix
14621462
if extension.lower() not in RESOURCE_EXTENSIONS:
14631463
raise ImportError(

packages/robot/src/robotcode/robot/diagnostics/library_doc.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from robot.variables.finders import VariableFinder
6767
from robot.variables.search import contains_variable
6868
from robotcode.core.lsp.types import Position, Range
69+
from robotcode.core.utils.path import normalized_path
6970
from robotcode.robot.diagnostics.entities import (
7071
ArgumentDefinition,
7172
ImportedVariableDefinition,
@@ -1490,7 +1491,7 @@ def _get_default_variables() -> Any:
14901491
if __default_variables is None:
14911492
__default_variables = Variables()
14921493
for k, v in {
1493-
"${TEMPDIR}": str(Path(tempfile.gettempdir()).resolve()),
1494+
"${TEMPDIR}": str(normalized_path(Path(tempfile.gettempdir()))),
14941495
"${/}": os.sep,
14951496
"${:}": os.pathsep,
14961497
"${\\n}": os.linesep,
@@ -2476,7 +2477,7 @@ def complete_library_import(
24762477
]
24772478

24782479
for p in paths:
2479-
path = p.resolve()
2480+
path = normalized_path(p)
24802481

24812482
if path.exists() and path.is_dir():
24822483
result += [
@@ -2541,9 +2542,9 @@ def complete_resource_import(
25412542
if name is None or name.startswith((".", "/", os.sep)):
25422543
name_path = Path(name if name else base_dir)
25432544
if name_path.is_absolute():
2544-
path = name_path.resolve()
2545+
path = normalized_path(name_path)
25452546
else:
2546-
path = Path(base_dir, name if name else base_dir).resolve()
2547+
path = normalized_path(Path(base_dir, name if name else base_dir))
25472548

25482549
if path.exists() and (path.is_dir()):
25492550
result += [
@@ -2592,7 +2593,7 @@ def complete_variables_import(
25922593
]
25932594

25942595
for p in paths:
2595-
path = p.resolve()
2596+
path = normalized_path(p)
25962597

25972598
if path.exists() and path.is_dir():
25982599
result += [

packages/runner/src/robotcode/runner/cli/discover/discover.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from robotcode.core.uri import Uri
4040
from robotcode.core.utils.cli import show_hidden_arguments
4141
from robotcode.core.utils.dataclasses import from_json
42+
from robotcode.core.utils.path import normalized_path
4243
from robotcode.plugin import (
4344
Application,
4445
OutputFormat,
@@ -273,7 +274,7 @@ def visit_suite(self, suite: TestSuite) -> None:
273274
self._collected[-1][suite.name] = True
274275
self._collected.append(NormalizedDict(ignore="_"))
275276
try:
276-
absolute_path = Path(suite.source).resolve() if suite.source else None
277+
absolute_path = normalized_path(Path(suite.source)) if suite.source else None
277278
item = TestItem(
278279
type="suite",
279280
id=f"{absolute_path or ''};{suite.longname}",
@@ -328,7 +329,7 @@ def visit_test(self, test: TestCase) -> None:
328329
if self._current.children is None:
329330
self._current.children = []
330331
try:
331-
absolute_path = Path(test.source).resolve() if test.source is not None else None
332+
absolute_path = normalized_path(Path(test.source)) if test.source is not None else None
332333
item = TestItem(
333334
type="test",
334335
id=f"{absolute_path or ''};{test.longname};{test.lineno}",
@@ -417,7 +418,7 @@ def add_diagnostic(
417418
line: Optional[int] = None,
418419
text: Optional[str] = None,
419420
) -> None:
420-
source_uri = str(Uri.from_path(Path(source_uri).resolve() if source_uri else Path.cwd()))
421+
source_uri = str(Uri.from_path(normalized_path(Path(source_uri)) if source_uri else Path.cwd()))
421422

422423
if source_uri not in result:
423424
result[source_uri] = []

0 commit comments

Comments
 (0)