Skip to content

Commit f975be8

Browse files
committed
feat(robotlangserver): Implement embedded keyword precedence for RF 6.0, this also speedups keyword analysing
1 parent e24603f commit f975be8

File tree

1 file changed

+143
-60
lines changed
  • robotcode/language_server/robotframework/diagnostics

1 file changed

+143
-60
lines changed

robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 143 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,7 +1713,7 @@ async def _find_keyword(self, name: Optional[str]) -> Optional[KeywordDoc]:
17131713
result = await self._get_explicit_keyword(name)
17141714

17151715
if not result:
1716-
result = await self._get_implicit_keyword(name)
1716+
result = self._get_implicit_keyword(name)
17171717

17181718
if not result and self.handle_bdd_style:
17191719
result = await self._get_bdd_style_keyword(name)
@@ -1723,27 +1723,54 @@ async def _find_keyword(self, name: Optional[str]) -> Optional[KeywordDoc]:
17231723
async def _get_keyword_from_self(self, name: str) -> Optional[KeywordDoc]:
17241724
if self.self_library_doc is None:
17251725
self.self_library_doc = await self.namespace.get_library_doc()
1726-
try:
1727-
return self.self_library_doc.keywords.get(name, None)
1728-
except KeywordError as e:
1729-
self.diagnostics.append(
1730-
DiagnosticsEntry(
1731-
str(e),
1732-
DiagnosticSeverity.ERROR,
1733-
"KeywordError",
1726+
1727+
if get_robot_version() >= (6, 0, 0):
1728+
found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = [
1729+
(None, v) for v in self.self_library_doc.keywords.get_all(name)
1730+
]
1731+
if len(found) > 1:
1732+
found = self._select_best_matches(found)
1733+
if len(found) > 1:
1734+
self.diagnostics.append(
1735+
DiagnosticsEntry(
1736+
self._create_multiple_keywords_found_message(name, found, implicit=False),
1737+
DiagnosticSeverity.ERROR,
1738+
"KeywordError",
1739+
)
1740+
)
1741+
raise CancelSearchError()
1742+
1743+
if len(found) == 1:
1744+
# TODO warning if keyword found is defined in resource and suite
1745+
return found[0][1]
1746+
1747+
return None
1748+
else:
1749+
try:
1750+
return self.self_library_doc.keywords.get(name, None)
1751+
except KeywordError as e:
1752+
self.diagnostics.append(
1753+
DiagnosticsEntry(
1754+
str(e),
1755+
DiagnosticSeverity.ERROR,
1756+
"KeywordError",
1757+
)
17341758
)
1735-
)
1736-
raise CancelSearchError() from e
1759+
raise CancelSearchError() from e
17371760

17381761
async def _yield_owner_and_kw_names(self, full_name: str) -> AsyncGenerator[Tuple[str, ...], None]:
17391762
tokens = full_name.split(".")
17401763
for i in range(1, len(tokens)):
17411764
yield ".".join(tokens[:i]), ".".join(tokens[i:])
17421765

17431766
async def _get_explicit_keyword(self, name: str) -> Optional[KeywordDoc]:
1744-
found: List[Tuple[LibraryEntry, KeywordDoc]] = []
1767+
found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
17451768
async for owner_name, kw_name in self._yield_owner_and_kw_names(name):
17461769
found.extend(await self.find_keywords(owner_name, kw_name))
1770+
1771+
if get_robot_version() >= (6, 0, 0) and len(found) > 1:
1772+
found = self._select_best_matches(found)
1773+
17471774
if len(found) > 1:
17481775
self.diagnostics.append(
17491776
DiagnosticsEntry(
@@ -1756,36 +1783,50 @@ async def _get_explicit_keyword(self, name: str) -> Optional[KeywordDoc]:
17561783

17571784
return found[0][1] if found else None
17581785

1759-
async def find_keywords(self, owner_name: str, name: str) -> Sequence[Tuple[LibraryEntry, KeywordDoc]]:
1786+
async def find_keywords(self, owner_name: str, name: str) -> List[Tuple[LibraryEntry, KeywordDoc]]:
17601787
if self._all_keywords is None:
17611788
self._all_keywords = [
17621789
v for v in chain(self.namespace._libraries.values(), self.namespace._resources.values())
17631790
]
1764-
return [
1765-
(v, v.library_doc.keywords[name])
1766-
for v in self._all_keywords
1767-
if eq(v.alias or v.name, owner_name) and name in v.library_doc.keywords
1768-
]
1791+
1792+
if get_robot_version() >= (6, 0, 0):
1793+
result: List[Tuple[LibraryEntry, KeywordDoc]] = []
1794+
for v in self._all_keywords:
1795+
if eq(v.alias or v.name, owner_name):
1796+
result.extend((v, kw) for kw in v.library_doc.keywords.get_all(name))
1797+
return result
1798+
else:
1799+
result = []
1800+
for v in self._all_keywords:
1801+
if eq(v.alias or v.name, owner_name):
1802+
kw = v.library_doc.keywords.get(name, None)
1803+
if kw is not None:
1804+
result.append((v, kw))
1805+
return result
17691806

17701807
def _create_multiple_keywords_found_message(
1771-
self, name: str, found: Sequence[Tuple[LibraryEntry, KeywordDoc]], implicit: bool = True
1808+
self, name: str, found: Sequence[Tuple[Optional[LibraryEntry], KeywordDoc]], implicit: bool = True
17721809
) -> str:
1810+
if any(e[1].is_embedded for e in found):
1811+
error = f"Multiple keywords matching name '{name}' found"
1812+
else:
1813+
error = f"Multiple keywords with name '{name}' found"
17731814

1774-
error = "Multiple keywords with name '%s' found" % name
1775-
if implicit:
1776-
error += ". Give the full name of the keyword you want to use"
1777-
names = sorted(f"{e[0].alias or e[0].name}.{e[1].name}" for e in found)
1815+
if implicit:
1816+
error += ". Give the full name of the keyword you want to use"
1817+
1818+
names = sorted(f"{e[1].name if e[0] is None else f'{e[0].alias or e[0].name}.{e[1].name}'}" for e in found)
17781819
return "\n ".join([error + ":"] + names)
17791820

1780-
async def _get_implicit_keyword(self, name: str) -> Optional[KeywordDoc]:
1781-
result = await self._get_keyword_from_resource_files(name)
1821+
def _get_implicit_keyword(self, name: str) -> Optional[KeywordDoc]:
1822+
result = self._get_keyword_from_resource_files(name)
17821823
if not result:
1783-
result = await self._get_keyword_from_libraries(name)
1824+
result = self._get_keyword_from_libraries(name)
17841825
return result
17851826

17861827
def _prioritize_same_file_or_public(
1787-
self, entries: List[Tuple[LibraryEntry, KeywordDoc]]
1788-
) -> List[Tuple[LibraryEntry, KeywordDoc]]:
1828+
self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
1829+
) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
17891830

17901831
matches = [h for h in entries if h[1].source == self.namespace.source]
17911832
if matches:
@@ -1796,8 +1837,8 @@ def _prioritize_same_file_or_public(
17961837
return matches or entries
17971838

17981839
def _select_best_matches(
1799-
self, entries: List[Tuple[LibraryEntry, KeywordDoc]]
1800-
) -> List[Tuple[LibraryEntry, KeywordDoc]]:
1840+
self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
1841+
) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
18011842

18021843
normal = [hand for hand in entries if not hand[1].is_embedded]
18031844
if normal:
@@ -1807,7 +1848,9 @@ def _select_best_matches(
18071848
return matches or entries
18081849

18091850
def _is_worse_match_than_others(
1810-
self, candidate: Tuple[LibraryEntry, KeywordDoc], alternatives: List[Tuple[LibraryEntry, KeywordDoc]]
1851+
self,
1852+
candidate: Tuple[Optional[LibraryEntry], KeywordDoc],
1853+
alternatives: List[Tuple[Optional[LibraryEntry], KeywordDoc]],
18111854
) -> bool:
18121855
for other in alternatives:
18131856
if (
@@ -1819,31 +1862,47 @@ def _is_worse_match_than_others(
18191862
return False
18201863

18211864
def _is_better_match(
1822-
self, candidate: Tuple[LibraryEntry, KeywordDoc], other: Tuple[LibraryEntry, KeywordDoc]
1865+
self, candidate: Tuple[Optional[LibraryEntry], KeywordDoc], other: Tuple[Optional[LibraryEntry], KeywordDoc]
18231866
) -> bool:
18241867
return (
18251868
other[1].matcher.embedded_arguments.match(candidate[1].name) is not None
18261869
and candidate[1].matcher.embedded_arguments.match(other[1].name) is None
18271870
)
18281871

1829-
async def _get_keyword_from_resource_files(self, name: str) -> Optional[KeywordDoc]:
1872+
def _get_keyword_from_resource_files(self, name: str) -> Optional[KeywordDoc]:
18301873
if self._resource_keywords is None:
18311874
self._resource_keywords = [v for v in chain(self.namespace._resources.values())]
18321875

1833-
found: List[Tuple[LibraryEntry, KeywordDoc]] = [
1834-
(v, v.library_doc.keywords[name]) for v in self._resource_keywords if name in v.library_doc.keywords
1835-
]
1876+
if get_robot_version() >= (6, 0, 0):
1877+
found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
1878+
for v in self._resource_keywords:
1879+
r = v.library_doc.keywords.get_all(name)
1880+
if r:
1881+
found.extend([(v, k) for k in r])
1882+
else:
1883+
found = []
1884+
for k in self._resource_keywords:
1885+
s = k.library_doc.keywords.get(name, None)
1886+
if s is not None:
1887+
found.append((k, s))
1888+
18361889
if not found:
18371890
return None
1891+
18381892
if get_robot_version() >= (6, 0, 0):
18391893
if len(found) > 1:
18401894
found = self._prioritize_same_file_or_public(found)
18411895

1896+
if len(found) > 1:
1897+
found = self._select_best_matches(found)
1898+
1899+
if len(found) > 1:
1900+
found = self._get_keyword_based_on_search_order(found)
1901+
1902+
else:
18421903
if len(found) > 1:
1843-
found = self._select_best_matches(found)
1904+
found = self._get_keyword_based_on_search_order(found)
18441905

1845-
if len(found) > 1:
1846-
found = await self._get_keyword_based_on_search_order(found)
18471906
if len(found) == 1:
18481907
return found[0][1]
18491908

@@ -1856,27 +1915,49 @@ async def _get_keyword_from_resource_files(self, name: str) -> Optional[KeywordD
18561915
)
18571916
raise CancelSearchError()
18581917

1859-
async def _get_keyword_based_on_search_order(
1860-
self, entries: List[Tuple[LibraryEntry, KeywordDoc]]
1861-
) -> List[Tuple[LibraryEntry, KeywordDoc]]:
1918+
def _get_keyword_based_on_search_order(
1919+
self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
1920+
) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
18621921

18631922
for libname in self.namespace.search_order:
18641923
for e in entries:
1865-
if eq(libname, e[0].alias or e[0].name):
1924+
if e[0] is not None and eq(libname, e[0].alias or e[0].name):
18661925
return [e]
18671926

18681927
return entries
18691928

1870-
async def _get_keyword_from_libraries(self, name: str) -> Optional[KeywordDoc]:
1929+
def _get_keyword_from_libraries(self, name: str) -> Optional[KeywordDoc]:
18711930
if self._library_keywords is None:
18721931
self._library_keywords = [v for v in chain(self.namespace._libraries.values())]
1873-
found = [(v, v.library_doc.keywords[name]) for v in self._library_keywords if name in v.library_doc.keywords]
1932+
1933+
if get_robot_version() >= (6, 0, 0):
1934+
found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
1935+
for v in self._library_keywords:
1936+
r = v.library_doc.keywords.get_all(name)
1937+
if r:
1938+
found.extend([(v, k) for k in r])
1939+
else:
1940+
found = []
1941+
1942+
for k in self._library_keywords:
1943+
s = k.library_doc.keywords.get(name, None)
1944+
if s is not None:
1945+
found.append((k, s))
1946+
18741947
if not found:
18751948
return None
1876-
if len(found) > 1:
1877-
found = await self._get_keyword_based_on_search_order(found)
1878-
if len(found) == 2:
1879-
found = await self._filter_stdlib_runner(*found)
1949+
1950+
if get_robot_version() >= (6, 0, 0):
1951+
if len(found) > 1:
1952+
found = self._select_best_matches(found)
1953+
if len(found) > 1:
1954+
found = self._get_keyword_based_on_search_order(found)
1955+
else:
1956+
if len(found) > 1:
1957+
found = self._get_keyword_based_on_search_order(found)
1958+
if len(found) == 2:
1959+
found = self._filter_stdlib_runner(*found)
1960+
18801961
if len(found) == 1:
18811962
return found[0][1]
18821963

@@ -1889,15 +1970,15 @@ async def _get_keyword_from_libraries(self, name: str) -> Optional[KeywordDoc]:
18891970
)
18901971
raise CancelSearchError()
18911972

1892-
async def _filter_stdlib_runner(
1893-
self, entry1: Tuple[LibraryEntry, KeywordDoc], entry2: Tuple[LibraryEntry, KeywordDoc]
1894-
) -> List[Tuple[LibraryEntry, KeywordDoc]]:
1973+
def _filter_stdlib_runner(
1974+
self, entry1: Tuple[Optional[LibraryEntry], KeywordDoc], entry2: Tuple[Optional[LibraryEntry], KeywordDoc]
1975+
) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
18951976
from robot.libraries import STDLIBS
18961977

18971978
stdlibs_without_remote = STDLIBS - {"Remote"}
1898-
if entry1[0].name in stdlibs_without_remote:
1979+
if entry1[0] is not None and entry1[0].name in stdlibs_without_remote:
18991980
standard, custom = entry1, entry2
1900-
elif entry2[0].name in stdlibs_without_remote:
1981+
elif entry2[0] is not None and entry2[0].name in stdlibs_without_remote:
19011982
standard, custom = entry2, entry1
19021983
else:
19031984
return [entry1, entry2]
@@ -1913,19 +1994,21 @@ async def _filter_stdlib_runner(
19131994
return [custom]
19141995

19151996
def _create_custom_and_standard_keyword_conflict_warning_message(
1916-
self, custom: Tuple[LibraryEntry, KeywordDoc], standard: Tuple[LibraryEntry, KeywordDoc]
1997+
self, custom: Tuple[Optional[LibraryEntry], KeywordDoc], standard: Tuple[Optional[LibraryEntry], KeywordDoc]
19171998
) -> str:
19181999
custom_with_name = standard_with_name = ""
1919-
if custom[0].alias is not None:
2000+
if custom[0] is not None and custom[0].alias is not None:
19202001
custom_with_name = " imported as '%s'" % custom[0].alias
1921-
if standard[0].alias is not None:
2002+
if standard[0] is not None and standard[0].alias is not None:
19222003
standard_with_name = " imported as '%s'" % standard[0].alias
19232004
return (
19242005
f"Keyword '{standard[1].name}' found both from a custom test library "
1925-
f"'{custom[0].name}'{custom_with_name} and a standard library '{standard[1].name}'{standard_with_name}. "
2006+
f"'{'' if custom[0] is None else custom[0].name}'{custom_with_name} "
2007+
f"and a standard library '{standard[1].name}'{standard_with_name}. "
19262008
f"The custom keyword is used. To select explicitly, and to get "
1927-
f"rid of this warning, use either '{custom[0].alias or custom[0].name}.{custom[1].name}' "
1928-
f"or '{standard[0].alias or standard[0].name}.{standard[1].name}'."
2009+
f"rid of this warning, use either "
2010+
f"'{'' if custom[0] is None else custom[0].alias or custom[0].name}.{custom[1].name}' "
2011+
f"or '{'' if standard[0] is None else standard[0].alias or standard[0].name}.{standard[1].name}'."
19292012
)
19302013

19312014
async def _get_bdd_style_keyword(self, name: str) -> Optional[KeywordDoc]:

0 commit comments

Comments
 (0)