Skip to content

Commit 3402437

Browse files
authored
Do not rename embedded variables in RenameKeywords (#428)
1 parent 74e6d6e commit 3402437

File tree

9 files changed

+124
-11
lines changed

9 files changed

+124
-11
lines changed

docs/releasenotes/3.3.1.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Robotidy 3.3.1
2+
=========================================
3+
Fix release for RenameKeyword variable matching improvements.
4+
5+
You can install the latest available version by running::
6+
7+
pip install --upgrade robotframework-tidy
8+
9+
or to install exactly this version::
10+
11+
pip install robotframework-tidy==3.3.1
12+
13+
14+
Do not rename variables in RenameKeyword (#417)
15+
------------------------------------------------
16+
Variables were also renamed in ``RenameKeyword`` transformer.
17+
Following code:
18+
19+
```
20+
Login With '${user.id}'
21+
```
22+
23+
was transformed to:
24+
25+
```
26+
Login With '${user.Id}'
27+
```
28+
29+
To fix this issue we redesigned keyword name matching in the transformer.
30+
Now it should properly escape all variables (and also possible indices).

robotidy/transformers/RenameKeywords.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional
44

55
from robot.api.parsing import ModelTransformer, Token
6+
from robot.variables.search import VariableIterator
67

78
from robotidy.disablers import skip_if_disabled, skip_section_if_disabled
89
from robotidy.exceptions import InvalidParameterValueError
@@ -86,21 +87,68 @@ def visit_Section(self, node): # noqa
8687
return self.generic_visit(node)
8788

8889
def rename_node(self, token, is_keyword_call):
90+
if self.replace_pattern is not None:
91+
new_value = self.rename_with_pattern(token.value, is_keyword_call=is_keyword_call)
92+
else:
93+
new_value = self.normalize_name(token.value, is_keyword_call=is_keyword_call)
94+
if not new_value.strip(): # do not allow renames that removes keywords altogether
95+
return
96+
token.value = new_value
97+
98+
def normalize_name(self, value, is_keyword_call):
99+
var_found = False
100+
parts = []
101+
new_name, remaining = "", ""
102+
for prefix, match, remaining in VariableIterator(value, ignore_errors=True):
103+
var_found = True
104+
# rename strips whitespace, so we need to preserve it if needed
105+
trailing_space = " " if prefix.endswith(" ") else ""
106+
remaining_space = " " if remaining.startswith(" ") else ""
107+
parts.extend([self.rename_part(prefix, is_keyword_call), trailing_space, match, remaining_space])
108+
if var_found:
109+
parts.append(self.rename_part(remaining, is_keyword_call))
110+
new_name = "".join(parts)
111+
else:
112+
new_name = self.rename_part(value, is_keyword_call)
113+
return new_name
114+
115+
def rename_part(self, part: str, is_keyword_call: bool):
89116
values = []
90-
split_names = token.value.split(".")
117+
split_names = part.split(".")
91118
for index, value in enumerate(split_names, start=1):
92119
if is_keyword_call and self.ignore_library and index != len(split_names):
93120
values.append(value)
94121
continue
95-
if self.replace_pattern is not None:
96-
value = self.replace_pattern.sub(repl=self.replace_to, string=value)
97-
if self.remove_underscores and set(value) != {"_"}:
98-
value = re.sub("_+", " ", value) # replace one or more _ with one space
99-
value = value.strip()
100-
# capitalize first letter of every word, leave rest untouched
101-
value = "".join([a if a.isupper() else b for a, b in zip(value, string.capwords(value))])
122+
value = self.remove_underscores_and_capitalize(value)
102123
values.append(value)
103-
token.value = ".".join(values)
124+
return ".".join(values)
125+
126+
def remove_underscores_and_capitalize(self, value: str):
127+
if self.remove_underscores:
128+
value = re.sub("_+", " ", value) # replace one or more _ with one space
129+
value = value.strip()
130+
# capitalize first letter of every word, leave rest untouched
131+
return "".join([a if a.isupper() else b for a, b in zip(value, string.capwords(value))])
132+
133+
def rename_with_pattern(self, value: str, is_keyword_call: bool):
134+
lib_name = ""
135+
if is_keyword_call and "." in value:
136+
# rename only non lib part
137+
found_lib = -1
138+
for prefix, _, _ in VariableIterator(value):
139+
found_lib = prefix.find(".")
140+
break
141+
if found_lib != -1:
142+
lib_name = value[: found_lib + 1]
143+
value = value[found_lib + 1 :]
144+
else:
145+
lib_name, value = value.split(".", maxsplit=1)
146+
lib_name += "."
147+
if lib_name and not self.ignore_library:
148+
lib_name = self.remove_underscores_and_capitalize(lib_name)
149+
return lib_name + self.remove_underscores_and_capitalize(
150+
self.replace_pattern.sub(repl=self.replace_to, string=value)
151+
)
104152

105153
@skip_if_disabled
106154
def visit_KeywordName(self, node): # noqa
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*** Keywords ***
2+
Embedded ${variables} That Should Be ${ignored.and.dots}
3+
Login With '{user.Uid}' And '${user.password}' To Check Validation
4+
5+
Variable With Square Brackets
6+
Normalize This${variable['test']}
7+
Normalize This${variable}['test']
8+
9+
Invalid Syntax
10+
Not Closed ${var
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*** Test Cases ***
2+
Library And Embedded Variable And Pattern
3+
LibraryName.With.Dots.New Name ${keyword} And ${var}

tests/atest/transformers/RenameKeywords/expected/rename_pattern_partial.robot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ All Upper Case
4747
Connect To System ABC
4848
Create Product DFG
4949

50-
Underscores And.Dots
50+
Underscores And. Dots
5151
Foo.BAR Baz A B C
5252
Foo Bar BAZ

tests/atest/transformers/RenameKeywords/expected/rename_pattern_whole.robot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ All Upper Case
4747
Connect To System ABC
4848
Create Product DFG
4949

50-
Underscores And.Dots
50+
Underscores And. Dots
5151
Foo.BAR Baz A B C
5252
Foo Bar BAZ
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*** Keywords ***
2+
Embedded ${variables} That should Be ${ignored.and.dots}
3+
Login With '{user.uid}'_and '${user.password}' to Check_Validation
4+
5+
Variable With Square Brackets
6+
normalize_this${variable['test']}
7+
normalize_this${variable}['test']
8+
9+
Invalid Syntax
10+
Not Closed ${var
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*** Test Cases ***
2+
Library And Embedded Variable And Pattern
3+
LibraryName.With.Dots.Rename with ${keyword} Variable

tests/atest/transformers/RenameKeywords/test_transformer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,12 @@ def test_disablers(self):
5858

5959
def test_run_keywords(self):
6060
self.compare(source="run_keywords.robot")
61+
62+
def test_embedded_variables(self):
63+
self.compare(source="embedded_variables.robot")
64+
65+
def test_embedded_with_pattern(self):
66+
self.compare(
67+
config=":replace_pattern=(?i)rename\s?with\s.+variable$:replace_to=New_Name_${keyword}_And_${var}",
68+
source="library_embedded_var_pattern.robot",
69+
)

0 commit comments

Comments
 (0)