Repository: MarketSquare/robotframework-robocop
Pull request #1401 (2025-05-14 13:08:14)

Variable type rule & formatters support
#1399 


Patch:
diff --git a/src/robocop/formatter/formatters/RenameVariables.py b/src/robocop/formatter/formatters/RenameVariables.py
index 76ac870..3c71856 100644
--- a/src/robocop/formatter/formatters/RenameVariables.py
+++ b/src/robocop/formatter/formatters/RenameVariables.py
@@ -13,6 +13,7 @@ from robocop.errors import InvalidParameterValueError
 from robocop.formatter.disablers import skip_if_disabled, skip_section_if_disabled
 from robocop.formatter.formatters import Formatter
 from robocop.formatter.utils import misc, variable_matcher
+from robocop.linter.utils.misc import remove_variable_type_conversion
 
 if TYPE_CHECKING:
     from robocop.formatter.skip import Skip
@@ -107,7 +108,7 @@ class VariablesScope:
         if len(variable) > 1 and variable[0] in "$@&" and variable[1] != "{":
             variable = f"{variable[0]}{{{variable[1:]}}}"
         match = search_variable(variable, ignore_errors=True)
-        return match.base
+        return remove_variable_type_conversion(match.base)
 
     def add_global(self, variable: str):
         var_name = self._get_var_name(variable)
diff --git a/src/robocop/linter/rules/misc.py b/src/robocop/linter/rules/misc.py
index 0ebfba4..28cd690 100644
--- a/src/robocop/linter/rules/misc.py
+++ b/src/robocop/linter/rules/misc.py
@@ -1099,7 +1099,8 @@ class SectionVariablesCollector(ast.NodeVisitor):
             return
         var_token = node.get_token(Token.VARIABLE)
         variable_match = search_variable(var_token.value, ignore_errors=True)
-        normalized = utils.normalize_robot_name(variable_match.base)
+        name = utils.remove_variable_type_conversion(variable_match.base)
+        normalized = utils.normalize_robot_name(name)
         self.section_variables[normalized] = CachedVariable(variable_match.name, var_token, is_used=False)
 
 
@@ -1195,13 +1196,12 @@ class UnusedVariablesChecker(VisitorChecker):
         for arg in node.get_tokens(Token.ARGUMENT):
             if arg.value[0] in ("@", "&"):  # ignore *args and &kwargs
                 continue
-            if "=" in arg.value:
-                arg_name, default_value = arg.value.split("=", maxsplit=1)
-                self.find_not_nested_variable(default_value, is_var=False)
-            else:
-                arg_name = arg.value
-            normalized_name = utils.normalize_robot_var_name(arg_name)
-            self.add_argument(arg_name, normalized_name, token=arg)
+            variable_match = search_variable(arg.value, ignore_errors=True)
+            if variable_match.after:
+                self.find_not_nested_variable(variable_match.after, is_var=False)
+            name = utils.remove_variable_type_conversion(variable_match.base)
+            normalized_name = utils.normalize_robot_name(name)
+            self.add_argument(variable_match.base, normalized_name, token=arg)
 
     def parse_embedded_arguments(self, name_token) -> None:
         """Store embedded arguments from keyword name. Ignore embedded variables patterns (${var:pattern})."""
@@ -1328,7 +1328,7 @@ class UnusedVariablesChecker(VisitorChecker):
         for token in node.header.get_tokens(Token.ARGUMENT, "OPTION"):  # Token.Option does not exist for RF3 and RF4
             self.find_not_nested_variable(token.value, is_var=False)
         for token in node.header.get_tokens(Token.VARIABLE):
-            self.handle_assign_variable(token)
+            self.handle_assign_variable(token, ignore_var_conversion=False)
         self.generic_visit(node)
         self.ignore_overwriting = False
         self.in_loop = False
@@ -1352,7 +1352,7 @@ class UnusedVariablesChecker(VisitorChecker):
         if self.try_assign(node) is not None:
             error_var = node.header.get_token(Token.VARIABLE)
             if error_var is not None:
-                self.handle_assign_variable(error_var)
+                self.handle_assign_variable(error_var, ignore_var_conversion=False)
         for item in node.body:
             self.visit(item)
         self.variables.pop()
@@ -1383,7 +1383,7 @@ class UnusedVariablesChecker(VisitorChecker):
         for argument in node.data_tokens:
             self.find_not_nested_variable(argument.value, is_var=False)
 
-    def handle_assign_variable(self, token) -> None:
+    def handle_assign_variable(self, token, ignore_var_conversion: bool = True) -> None:
         """
         Check if assign does not overwrite arguments or variables.
 
@@ -1391,7 +1391,10 @@ class UnusedVariablesChecker(VisitorChecker):
         """
         value = token.value
         variable_match = search_variable(value, ignore_errors=True)
-        normalized = utils.normalize_robot_name(variable_match.base)
+        name = variable_match.base
+        if ignore_var_conversion:
+            name = utils.remove_variable_type_conversion(name)
+        normalized = utils.normalize_robot_name(name)
         if not normalized:  # ie. "${_}" -> ""
             return
         arg = self.arguments.get(normalized, None)
@@ -1407,7 +1410,9 @@ class UnusedVariablesChecker(VisitorChecker):
                 is_used = variable_scope[normalized].is_used
                 if not variable_scope[normalized].is_used and not self.ignore_overwriting:
                     self.report_arg_or_var_rule(
-                        self.variable_overwritten_before_usage, variable_scope[normalized].token, variable_match.name
+                        self.variable_overwritten_before_usage,
+                        variable_scope[normalized].token,
+                        variable_scope[normalized].name,
                     )
             else:  # check for attribute access like .lower() or .x
                 for variable_scope in self.variables[::-1]:
diff --git a/src/robocop/linter/rules/naming.py b/src/robocop/linter/rules/naming.py
index 1b34e8e..41087be 100644
--- a/src/robocop/linter/rules/naming.py
+++ b/src/robocop/linter/rules/naming.py
@@ -16,18 +16,7 @@ from robot.variables.search import search_variable
 
 from robocop.linter import sonar_qube
 from robocop.linter.rules import Rule, RuleParam, RuleSeverity, VisitorChecker, deprecated, variables
-from robocop.linter.utils.misc import (
-    ROBOT_VERSION,
-    find_robot_vars,
-    is_var_scope_local,
-    keyword_col,
-    normalize_robot_name,
-    normalize_robot_var_name,
-    pattern_type,
-    remove_nested_variables,
-    remove_robot_vars,
-    token_col,
-)
+from robocop.linter.utils import misc as utils
 from robocop.linter.utils.variable_matcher import VariableMatches
 from robocop.parsing.run_keywords import iterate_keyword_names
 
@@ -57,7 +46,7 @@ class NotAllowedCharInNameRule(Rule):
         RuleParam(
             name="pattern",
             default=re.compile(r"[\.\?]"),
-            converter=pattern_type,
+            converter=utils.pattern_type,
             show_type="regex",
             desc="pattern defining characters (not) allowed in a name",
         )
@@ -119,7 +108,7 @@ class WrongCaseInKeywordNameRule(Rule):
         RuleParam(
             name="pattern",
             default=re.compile(r""),
-            converter=pattern_type,
+            converter=utils.pattern_type,
             show_type="regex",
             desc="pattern for accepted words in keyword",
         ),
@@ -497,7 +486,7 @@ class NotAllowedCharInFilenameRule(Rule):
         RuleParam(
             name="pattern",
             default=re.compile(r"[\.\?]"),
-            converter=pattern_type,
+            converter=utils.pattern_type,
             show_type="regex",
             desc="pattern defining characters (not) allowed in a name",
         ),
@@ -593,7 +582,7 @@ class InvalidCharactersInNameChecker(VisitorChecker):
         Skips embedded variables from keyword name
         """
         node_name = node.name
-        robot_vars = find_robot_vars(node_name) if is_keyword else []
+        robot_vars = utils.find_robot_vars(node_name) if is_keyword else []
         start_pos = 0
         for variable in robot_vars:
             # Loop and skip variables:
@@ -696,7 +685,7 @@ class KeywordNamingChecker(VisitorChecker):
 
     def visit_KeywordCall(self, node) -> None:  # noqa: N802
         if self.inside_if_block and node.keyword and node.keyword.lower() in self.else_statements:
-            col = keyword_col(node)
+            col = utils.keyword_col(node)
             end_col = col + len(node.keyword)
             self.report(self.else_not_upper_case, node=node, col=col, end_col=end_col)
         self.check_keyword_naming_with_subkeywords(node, Token.KEYWORD)
@@ -714,7 +703,7 @@ class KeywordNamingChecker(VisitorChecker):
             return
         if self.check_if_keyword_is_reserved(keyword_name, node):
             return
-        normalized = remove_robot_vars(keyword_name)
+        normalized = utils.remove_robot_vars(keyword_name)
         normalized = self.wrong_case_in_keyword_name.pattern.sub("", normalized)
         normalized = normalized.split(".")[-1]  # remove any imports ie ExternalLib.SubLib.Log -> Log
         normalized = normalized.replace("'", "")  # replace ' apostrophes
@@ -743,7 +732,7 @@ class KeywordNamingChecker(VisitorChecker):
             return
         arg = node.get_token(Token.ARGUMENT)
         suffix = f". Use one space between: '{keyword_name.title()} {arg.value}'" if arg else ""
-        col = token_col(node, Token.NAME, Token.KEYWORD)
+        col = utils.token_col(node, Token.NAME, Token.KEYWORD)
         end_col = col + len(keyword_name)
         self.report(
             self.bdd_without_keyword_call,
@@ -762,7 +751,7 @@ class KeywordNamingChecker(VisitorChecker):
         if lower_name in self.else_statements and self.inside_if_block:
             return False  # handled by else-not-upper-case
         min_ver = self.reserved_words[lower_name]
-        if ROBOT_VERSION.major < min_ver:
+        if utils.ROBOT_VERSION.major < min_ver:
             return False
         error_msg = uppercase_error_msg(lower_name)
         self.report(
@@ -786,9 +775,9 @@ class SettingsNamingChecker(VisitorChecker):
     invalid_section: InvalidSectionRule
     mixed_task_test_settings: MixedTaskTestSettingsRule
 
-    ALIAS_TOKENS = [Token.WITH_NAME] if ROBOT_VERSION.major < 5 else ["WITH NAME", "AS"]
+    ALIAS_TOKENS = [Token.WITH_NAME] if utils.ROBOT_VERSION.major < 5 else ["WITH NAME", "AS"]
     # Separating alias values since RF 3 uses WITH_NAME instead of WITH NAME
-    ALIAS_TOKENS_VALUES = ["WITH NAME"] if ROBOT_VERSION.major < 5 else ["WITH NAME", "AS"]
+    ALIAS_TOKENS_VALUES = ["WITH NAME"] if utils.ROBOT_VERSION.major < 5 else ["WITH NAME", "AS"]
 
     def __init__(self):
         self.section_name_pattern = re.compile(r"\*\*\*\s.+\s\*\*\*")
@@ -825,8 +814,8 @@ class SettingsNamingChecker(VisitorChecker):
         self.task_section = None
         for section in node.sections:
             if isinstance(section, TestCaseSection):
-                if (ROBOT_VERSION.major < 6 and "task" in section.header.name.lower()) or (
-                    ROBOT_VERSION.major >= 6 and section.header.type == Token.TASK_HEADER
+                if (utils.ROBOT_VERSION.major < 6 and "task" in section.header.name.lower()) or (
+                    utils.ROBOT_VERSION.major >= 6 and section.header.type == Token.TASK_HEADER
                 ):
                     self.task_section = True
                 else:
@@ -848,7 +837,7 @@ class SettingsNamingChecker(VisitorChecker):
 
     def visit_LibraryImport(self, node) -> None:  # noqa: N802
         self.check_setting_name(node.data_tokens[0].value, node)
-        if ROBOT_VERSION.major < 6:
+        if utils.ROBOT_VERSION.major < 6:
             arg_nodes = node.get_tokens(Token.ARGUMENT)
             # ignore cases where 'AS' is used to provide library alias for RF < 5
             if arg_nodes and any(arg.value == "AS" for arg in arg_nodes):
@@ -980,6 +969,7 @@ class VariableNamingChecker(VisitorChecker):
             return  # TODO: Ignore for now, for example ${not  closed in variables will throw it
         if var_name is None:
             return  # in RF<=5, a continuation mark ` ...` is wrongly considered a variable
+        var_name = utils.remove_variable_type_conversion(var_name)
         # in Variables section, everything needs to be in uppercase
         # because even when the variable is nested, it needs to be global
         if not var_name.isupper():
@@ -997,7 +987,7 @@ class VariableNamingChecker(VisitorChecker):
             self.check_for_reserved_naming_or_hyphen(token, "Variable")
         if not node.keyword:
             return
-        if normalize_robot_name(node.keyword, remove_prefix="builtin.") in SET_VARIABLE_VARIANTS:
+        if utils.normalize_robot_name(node.keyword, remove_prefix="builtin.") in SET_VARIABLE_VARIANTS:
             if len(node.data_tokens) < 2:
                 return
             token = node.data_tokens[1]
@@ -1012,7 +1002,7 @@ class VariableNamingChecker(VisitorChecker):
             self.check_non_local_variable(var_name, node, token)
 
     def check_non_local_variable(self, variable_name: str, node, token) -> None:
-        normalized_var_name = remove_nested_variables(variable_name)
+        normalized_var_name = utils.remove_nested_variables(variable_name)
         if not normalized_var_name:
             return
         # a variable as a keyword argument can contain lowercase nested variable
@@ -1033,7 +1023,7 @@ class VariableNamingChecker(VisitorChecker):
             return
         self.check_for_reserved_naming_or_hyphen(variable, "Variable")
         # TODO: Check supported syntax for variable, ie ${{var}}?
-        if not is_var_scope_local(node):
+        if not utils.is_var_scope_local(node):
             self.check_non_local_variable(search_variable(variable.value).base, node, variable)
 
     def visit_If(self, node) -> None:  # noqa: N802
@@ -1072,7 +1062,7 @@ class VariableNamingChecker(VisitorChecker):
             )
         if variable_match.items:  # item assignments ${dict}[key] =
             return
-        normalized_name = normalize_robot_name(name)
+        normalized_name = utils.normalize_robot_name(name)
         if normalized_name in self.RESERVED_VARIABLES:
             reserved_variable = self.RESERVED_VARIABLES[normalized_name]
             self.report(
@@ -1115,11 +1105,11 @@ class SimilarVariableChecker(VisitorChecker):
         self.generic_visit(node)
 
     def visit_KeywordCall(self, node) -> None:  # noqa: N802
-        if normalize_robot_name(node.keyword, remove_prefix="builtin.") in SET_VARIABLE_VARIANTS:
+        if utils.normalize_robot_name(node.keyword, remove_prefix="builtin.") in SET_VARIABLE_VARIANTS:
             normalized, assign_value = "", ""
             for index, token in enumerate(node.data_tokens[1:]):
                 if index == 0:  # First argument is assign-like
-                    normalized = normalize_robot_var_name(token.value)
+                    normalized = utils.normalize_robot_var_name(token.value)
                     assign_value = token.value  # process assign last, cache for now
                 else:
                     self.find_not_nested_variable(token, token.value, is_var=False)
@@ -1137,7 +1127,7 @@ class SimilarVariableChecker(VisitorChecker):
             self.find_not_nested_variable(arg, arg.value, is_var=False)
         variable = node.get_token(Token.VARIABLE)
         if variable:
-            self.find_similar_variables([variable], node, ignore_overwriting=not is_var_scope_local(node))
+            self.find_similar_variables([variable], node, ignore_overwriting=not utils.is_var_scope_local(node))
 
     def visit_If(self, node) -> None:  # noqa: N802
         for token in node.header.get_tokens(Token.ARGUMENT):
@@ -1153,7 +1143,7 @@ class SimilarVariableChecker(VisitorChecker):
 
     @staticmethod
     def for_assign_vars(for_node) -> Iterable[str]:
-        if ROBOT_VERSION.major < 7:
+        if utils.ROBOT_VERSION.major < 7:
             yield from for_node.variables
         else:
             yield from for_node.assign
@@ -1162,7 +1152,7 @@ class SimilarVariableChecker(VisitorChecker):
         for token in node.header.get_tokens(Token.ARGUMENT):
             self.find_not_nested_variable(token, token.value, is_var=False)
         for var in self.for_assign_vars(node):
-            self.assigned_variables[normalize_robot_var_name(var)].append(var)
+            self.assigned_variables[utils.normalize_robot_var_name(var)].append(var)
         self.generic_visit(node)
 
     visit_ForLoop = visit_For  # noqa: N815
@@ -1181,7 +1171,7 @@ class SimilarVariableChecker(VisitorChecker):
                     var_name, *pattern = token.value.split(":", maxsplit=1)
                     if pattern:
                         var_name = var_name + "}"  # recreate, so it handles ${variable:pattern} -> ${variable} matching
-                    normalized_name = normalize_robot_var_name(var_name)
+                    normalized_name = utils.normalize_robot_var_name(var_name)
                     self.assigned_variables[normalized_name].append(var_name)
         except VariableError:
             pass
@@ -1193,7 +1183,7 @@ class SimilarVariableChecker(VisitorChecker):
         :param value: name of variable found in token value string
         :param offset: starting position of variable in token value string
         """
-        normalized = normalize_robot_name(value)
+        normalized = utils.normalize_robot_name(value)
         if normalized not in self.assigned_variables:
             return  # we could handle attr access here, ignoring now
         latest_assign = self.assigned_variables[normalized][-1]
@@ -1254,14 +1244,14 @@ class SimilarVariableChecker(VisitorChecker):
                 for token in child.get_tokens(Token.ARGUMENT):
                     variable_match = search_variable(token.value, ignore_errors=True)
                     name = variable_match.name
-                    normalized = normalize_robot_name(variable_match.base)
+                    normalized = utils.normalize_robot_name(variable_match.base)
                     self.assigned_variables[normalized].append(name)
 
     def find_similar_variables(self, tokens, node, ignore_overwriting: bool = False) -> None:
         for token in tokens:
             variable_match = search_variable(token.value, ignore_errors=True)
             name = variable_match.name
-            normalized = normalize_robot_name(variable_match.base)
+            normalized = utils.normalize_robot_name(variable_match.base)
             if (
                 not ignore_overwriting
                 and normalized in self.assigned_variables
@@ -1346,7 +1336,7 @@ class DeprecatedStatementChecker(VisitorChecker):
 
     def visit_Return(self, node) -> None:  # noqa: N802
         """For RETURN use visit_ReturnStatement - visit_Return will most likely visit RETURN in the future"""
-        if ROBOT_VERSION.major not in (5, 6):
+        if utils.ROBOT_VERSION.major not in (5, 6):
             return
         self.check_deprecated_return(node)
 
@@ -1359,13 +1349,13 @@ class DeprecatedStatementChecker(VisitorChecker):
             statement_name="[Return]",
             alternative="RETURN",
             node=node,
-            col=token_col(node, Token.RETURN),
+            col=utils.token_col(node, Token.RETURN),
             end_col=node.end_col_offset,
             version="5.*",
         )
 
     def visit_ForceTags(self, node) -> None:  # noqa: N802
-        if ROBOT_VERSION.major < 6:
+        if utils.ROBOT_VERSION.major < 6:
             return
         setting_name = node.data_tokens[0].value.lower()
         if setting_name == "force tags":
@@ -1374,19 +1364,19 @@ class DeprecatedStatementChecker(VisitorChecker):
                 statement_name="Force Tags",
                 alternative="Test Tags",
                 node=node,
-                col=token_col(node, Token.FORCE_TAGS),
+                col=utils.token_col(node, Token.FORCE_TAGS),
                 end_col=node.col_offset + len(setting_name) + 1,
                 version="6.0",
             )
 
     def check_if_keyword_is_deprecated(self, keyword_name, node) -> None:
-        normalized_keyword_name = normalize_robot_name(keyword_name, remove_prefix="builtin.")
+        normalized_keyword_name = utils.normalize_robot_name(keyword_name, remove_prefix="builtin.")
         if normalized_keyword_name not in self.deprecated_keywords:
             return
         version, alternative = self.deprecated_keywords[normalized_keyword_name]
-        if version > ROBOT_VERSION.major:
+        if version > utils.ROBOT_VERSION.major:
             return
-        col = token_col(node, Token.NAME, Token.KEYWORD)
+        col = utils.token_col(node, Token.NAME, Token.KEYWORD)
         self.report(
             self.deprecated_statement,
             statement_name=keyword_name,
@@ -1398,10 +1388,10 @@ class DeprecatedStatementChecker(VisitorChecker):
         )
 
     def check_keyword_can_be_replaced_with_var(self, keyword_name, node) -> None:
-        if ROBOT_VERSION.major < 7:
+        if utils.ROBOT_VERSION.major < 7:
             return
-        normalized = normalize_robot_name(keyword_name, remove_prefix="builtin.")
-        col = token_col(node, Token.NAME, Token.KEYWORD)
+        normalized = utils.normalize_robot_name(keyword_name, remove_prefix="builtin.")
+        col = utils.token_col(node, Token.NAME, Token.KEYWORD)
         if normalized in self.set_variable_keywords:
             self.report(
                 self.replace_set_variable_with_var,
@@ -1420,7 +1410,7 @@ class DeprecatedStatementChecker(VisitorChecker):
             )
 
     def visit_LibraryImport(self, node) -> None:  # noqa: N802
-        if ROBOT_VERSION.major < 5 or (ROBOT_VERSION.major == 5 and ROBOT_VERSION.minor == 0):
+        if utils.ROBOT_VERSION.major < 5 or (utils.ROBOT_VERSION.major == 5 and utils.ROBOT_VERSION.minor == 0):
             return
         with_name_token = node.get_token(Token.WITH_NAME)
         if not with_name_token or with_name_token.value == "AS":
diff --git a/src/robocop/linter/utils/misc.py b/src/robocop/linter/utils/misc.py
index 6d428bd..48f7490 100644
--- a/src/robocop/linter/utils/misc.py
+++ b/src/robocop/linter/utils/misc.py
@@ -75,6 +75,11 @@ def rf_supports_lang() -> bool:
     return ROBOT_VERSION >= ROBOT_WITH_LANG
 
 
+def remove_variable_type_conversion(name: str) -> str:
+    name, *_ = name.split(": ", maxsplit=1)
+    return name
+
+
 def normalize_robot_name(name: str, remove_prefix: str | None = None) -> str:
     name = name.replace(" ", "").replace("_", "").lower() if name else ""
     if remove_prefix:
diff --git a/uv.lock b/uv.lock
index d3e1ff9..196d0b1 100644
--- a/uv.lock
+++ b/uv.lock
@@ -542,10 +542,11 @@ dependencies = [
     { name = "pathspec" },
     { name = "platformdirs" },
     { name = "pytz" },
+    { name = "rich" },
     { name = "robotframework" },
     { name = "tomli", marker = "python_full_version < '3.11'" },
     { name = "tomli-w" },
-    { name = "typer" },
+    { name = "typer-slim" },
 ]
 
 [package.dev-dependencies]
@@ -572,10 +573,11 @@ requires-dist = [
     { name = "pathspec", specifier = ">=0.12" },
     { name = "platformdirs", specifier = ">=4.3" },
     { name = "pytz", specifier = ">=2022.7" },
+    { name = "rich", specifier = ">=10.11.0" },
     { name = "robotframework", specifier = ">=4.0,<7.3" },
     { name = "tomli", marker = "python_full_version < '3.11'", specifier = "==2.0.2" },
     { name = "tomli-w", specifier = ">=1.0" },
-    { name = "typer", specifier = ">=0.12.5" },
+    { name = "typer-slim", specifier = ">=0.12.5" },
 ]
 
 [package.metadata.requires-dev]
@@ -628,15 +630,6 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763 },
 ]
 
-[[package]]
-name = "shellingham"
-version = "1.5.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
-]
-
 [[package]]
 name = "snowballstemmer"
 version = "2.2.0"
@@ -878,18 +871,16 @@ wheels = [
 ]
 
 [[package]]
-name = "typer"
-version = "0.15.2"
+name = "typer-slim"
+version = "0.15.3"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "click" },
-    { name = "rich" },
-    { name = "shellingham" },
     { name = "typing-extensions" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
+sdist = { url = "https://files.pythonhosted.org/packages/4a/62/b569dc0bdaf733250afb4693f00a88ef16232b19f8b6b6c3ef1c245e33a7/typer_slim-0.15.3.tar.gz", hash = "sha256:c134ae789afe8957c3f5f420eed07df53621271aca192b8e497d43a29b94c878", size = 101634 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
+    { url = "https://files.pythonhosted.org/packages/92/b1/0f3d4f3e35fd82248ed5bcb70c6249baebb39f0ab3faf7f9818101eec991/typer_slim-0.15.3-py3-none-any.whl", hash = "sha256:626c125b1b01eebafdc2bb12326b401863d1c4752342e3f9c1d9f829939fe300", size = 45310 },
 ]
 
 [[package]]


Test patch:
diff --git a/tests/formatter/formatters/RenameVariables/expected/variable_type_conversion.robot b/tests/formatter/formatters/RenameVariables/expected/variable_type_conversion.robot
new file mode 100644
index 0000000..659f524
--- /dev/null
+++ b/tests/formatter/formatters/RenameVariables/expected/variable_type_conversion.robot
@@ -0,0 +1,80 @@
+*** Variables ***
+# Simple type.
+${VERSION: float}         7.3
+# Parameterized type.
+${CRITICAL: list[int]}    [3278, 5368, 5417]
+# With @{list} variables the type specified the item type.
+@{HIGH: int}              4173    5334    5386    5387
+@{HIGH_LOW: int}              4173    5334    5386    5387
+# With @{dict} variables the type specified the value type.
+&{DA_TES: date}            rc1=2025-05-08    final=2025-05-15
+# Alternative syntax to specify both key and value types.
+&{NUMBERS: int=float}     1=2.3    4=5.6
+
+
+*** Test Cases ***
+Variables section
+    # Validate above variables using the inline Python evaluation syntax.
+    # This syntax is much more complicated than the syntax used above!
+    Should Be Equal    ${VERSION}       ${{7.3}}
+    Should Be Equal    ${CRITICAL}      ${{[3278, 5368, 5417]}}
+    Should Be Equal    ${HIGH}          ${{[4173, 5334, 5386, 5387]}}
+    Should Be Equal    ${DATES}         ${{{'rc1': datetime.date(2025, 5, 8), 'final': datetime.date(2025, 5, 15)}}}
+    Should Be Equal    ${NUMBERS}       ${{{1: 2.3, 4: 5.6}}}
+
+VAR syntax
+    # The VAR syntax supports types the same way as the Variables section
+    VAR    ${number: int}      42
+    Should Be Equal    ${number}    ${42}
+
+Assignment
+    [Tags]    robot:continue-on-failure
+    # In simple cases the VAR syntax is more convenient.
+    ${number: int} =    Set Variable    42
+    Should Be Equal    ${number}    ${42}
+
+    # we can also convert other keywords output
+    ${number: str}    Convert To Integer    42
+    Should Be Equal    ${number}    ${42}
+
+    # or even convert to list
+    ${variable: list[str]}    Set Variable    42    43
+    Should Be Equal    ${variable}    ${42}
+
+    # In this example conversion is more useful.
+    ${match}    ${version: float} =    Should Match Regexp    RF 7.3    ^RF (\\d+\\.\\d+)$
+    Should Be Equal    ${match}      RF 7.3
+    Should Be Equal    ${version}    ${7.3}
+
+Keyword arguments
+    # Argument conversion with user keywords is very convenient!
+    Move    10    down    slow=no
+    # Conversion handles validation automatically. This usage fails.
+    Move    10    invalid
+
+Embedded argumemts
+    # Also embedded arguments can be converted.
+    Move 3.14 meters
+
+Other
+    Blocks
+
+*** Keywords ***
+Move
+    [Arguments]    ${distance: int}    ${direction: Literal["UP", "DOWN"]}    ${slow: bool}=True
+    Should Be Equal    ${distance}     ${10}
+    Should Be Equal    ${direction}    DOWN
+    Should Be Equal    ${slow}         ${False}
+
+Move ${distance: int | float} meters
+    Should Be Equal    ${distance}     ${3.14}
+
+Return Variable
+    [Arguments]    ${variable}
+    RETURN    ${variable}
+
+Blocks
+    # 'var: int' will be unused, since FOR does not support var type conversion
+    FOR    ${var: int}    IN    @{LIST_OF_STRINGS}
+        Type Should Be    ${var}    int
+    END
diff --git a/tests/formatter/formatters/RenameVariables/source/variable_type_conversion.robot b/tests/formatter/formatters/RenameVariables/source/variable_type_conversion.robot
new file mode 100644
index 0000000..57bd5c7
--- /dev/null
+++ b/tests/formatter/formatters/RenameVariables/source/variable_type_conversion.robot
@@ -0,0 +1,80 @@
+*** Variables ***
+# Simple type.
+${version: float}         7.3
+# Parameterized type.
+${CRITICAL: list[int]}    [3278, 5368, 5417]
+# With @{list} variables the type specified the item type.
+@{HIGH: int}              4173    5334    5386    5387
+@{HIGH low: int}              4173    5334    5386    5387
+# With @{dict} variables the type specified the value type.
+&{DATes: date}            rc1=2025-05-08    final=2025-05-15
+# Alternative syntax to specify both key and value types.
+&{NUMBERS: int=float}     1=2.3    4=5.6
+
+
+*** Test Cases ***
+Variables section
+    # Validate above variables using the inline Python evaluation syntax.
+    # This syntax is much more complicated than the syntax used above!
+    Should Be Equal    ${VERSION}       ${{7.3}}
+    Should Be Equal    ${CRITICAL}      ${{[3278, 5368, 5417]}}
+    Should Be Equal    ${HIGH}          ${{[4173, 5334, 5386, 5387]}}
+    Should Be Equal    ${DATES}         ${{{'rc1': datetime.date(2025, 5, 8), 'final': datetime.date(2025, 5, 15)}}}
+    Should Be Equal    ${NUMBERS}       ${{{1: 2.3, 4: 5.6}}}
+
+VAR syntax
+    # The VAR syntax supports types the same way as the Variables section
+    VAR    ${number: int}      42
+    Should Be Equal    ${number}    ${42}
+
+Assignment
+    [Tags]    robot:continue-on-failure
+    # In simple cases the VAR syntax is more convenient.
+    ${number: int} =    Set Variable    42
+    Should Be Equal    ${number}    ${42}
+
+    # we can also convert other keywords output
+    ${number: str}    Convert To Integer    42
+    Should Be Equal    ${number}    ${42}
+
+    # or even convert to list
+    ${variable: list[str]}    Set Variable    42    43
+    Should Be Equal    ${variable}    ${42}
+
+    # In this example conversion is more useful.
+    ${match}    ${version: float} =    Should Match Regexp    RF 7.3    ^RF (\\d+\\.\\d+)$
+    Should Be Equal    ${match}      RF 7.3
+    Should Be Equal    ${version}    ${7.3}
+
+Keyword arguments
+    # Argument conversion with user keywords is very convenient!
+    Move    10    down    slow=no
+    # Conversion handles validation automatically. This usage fails.
+    Move    10    invalid
+
+Embedded argumemts
+    # Also embedded arguments can be converted.
+    Move 3.14 meters
+
+Other
+    Blocks
+
+*** Keywords ***
+Move
+    [Arguments]    ${distance: int}    ${direction: Literal["UP", "DOWN"]}    ${slow: bool}=True
+    Should Be Equal    ${distance}     ${10}
+    Should Be Equal    ${direction}    DOWN
+    Should Be Equal    ${slow}         ${False}
+
+Move ${distance: int | float} meters
+    Should Be Equal    ${distance}     ${3.14}
+
+Return Variable
+    [Arguments]    ${variable}
+    RETURN    ${variable}
+
+Blocks
+    # 'var: int' will be unused, since FOR does not support var type conversion
+    FOR    ${var: int}    IN    @{list_of_strings}
+        Type Should Be    ${var}    int
+    END
diff --git a/tests/formatter/formatters/RenameVariables/test_formatter.py b/tests/formatter/formatters/RenameVariables/test_formatter.py
index 20baa05..6775491 100644
--- a/tests/formatter/formatters/RenameVariables/test_formatter.py
+++ b/tests/formatter/formatters/RenameVariables/test_formatter.py
@@ -141,3 +141,6 @@ class TestRenameVariables(FormatterAcceptanceTest):
 
     def test_template_arguments(self):
         self.compare(source="template_arguments.robot")
+
+    def test_variable_type_conversion(self):
+        self.compare(source="variable_type_conversion.robot", test_on_version=">7.2")
diff --git a/tests/linter/rules/arguments/argument_overwritten_before_usage/test_rule.py b/tests/linter/rules/arguments/argument_overwritten_before_usage/test_rule.py
index ab51533..20544d9 100644
--- a/tests/linter/rules/arguments/argument_overwritten_before_usage/test_rule.py
+++ b/tests/linter/rules/arguments/argument_overwritten_before_usage/test_rule.py
@@ -52,3 +52,10 @@ class TestRuleAcceptance(RuleAcceptance):
             configure=["argument-overwritten-before-usage.unknown=value"], exit_code=2, compare_output=False
         )
         assert output == expected
+
+    def test_variable_type_conversion(self):
+        self.check_rule(
+            src_files=["variable_type_conversion.robot"],
+            expected_file="variable_type_conversion_expected.txt",
+            # test_on_version=">=7.3" FIXME
+        )
diff --git a/tests/linter/rules/arguments/argument_overwritten_before_usage/variable_type_conversion.robot b/tests/linter/rules/arguments/argument_overwritten_before_usage/variable_type_conversion.robot
new file mode 100644
index 0000000..7d92ee2
--- /dev/null
+++ b/tests/linter/rules/arguments/argument_overwritten_before_usage/variable_type_conversion.robot
@@ -0,0 +1,9 @@
+*** Keywords ***
+Move
+    [Arguments]    ${distance: int}    ${direction: Literal["UP", "DOWN"]}    ${slow: bool}=True
+    VAR    ${distance}     ${10}
+    VAR    ${direction}    DOWN
+    VAR    ${slow}         ${False}
+
+Move ${distance: int | float} meters
+    VAR    ${distance}     ${3.14}
diff --git a/tests/linter/rules/arguments/argument_overwritten_before_usage/variable_type_conversion_expected.txt b/tests/linter/rules/arguments/argument_overwritten_before_usage/variable_type_conversion_expected.txt
new file mode 100644
index 0000000..76d6c8a
--- /dev/null
+++ b/tests/linter/rules/arguments/argument_overwritten_before_usage/variable_type_conversion_expected.txt
@@ -0,0 +1,4 @@
+variable_type_conversion.robot:3:20 [W] ARG02 Keyword argument '${distance: int}' is overwritten before usage
+variable_type_conversion.robot:3:40 [W] ARG02 Keyword argument '${direction: Literal["UP", "DOWN"]}' is overwritten before usage
+variable_type_conversion.robot:3:79 [W] ARG02 Keyword argument '${slow: bool}=True' is overwritten before usage
+variable_type_conversion.robot:8:6 [W] ARG02 Keyword argument '${distance: int | float}' is overwritten before usage
\ No newline at end of file
diff --git a/tests/linter/rules/arguments/unused_argument/test_rule.py b/tests/linter/rules/arguments/unused_argument/test_rule.py
index 15699ba..273ec53 100644
--- a/tests/linter/rules/arguments/unused_argument/test_rule.py
+++ b/tests/linter/rules/arguments/unused_argument/test_rule.py
@@ -33,3 +33,10 @@ class TestRuleAcceptance(RuleAcceptance):
             src_files=["extended_variable_syntax.robot"],
             expected_file=None,
         )
+
+    def test_variable_type_conversion(self):
+        self.check_rule(
+            src_files=["variable_type_conversion.robot"],
+            expected_file="variable_type_conversion_expected.txt",
+            # test_on_version=">=7.3" FIXME
+        )
diff --git a/tests/linter/rules/arguments/unused_argument/variable_type_conversion.robot b/tests/linter/rules/arguments/unused_argument/variable_type_conversion.robot
new file mode 100644
index 0000000..1d69c56
--- /dev/null
+++ b/tests/linter/rules/arguments/unused_argument/variable_type_conversion.robot
@@ -0,0 +1,7 @@
+*** Keywords ***
+Move
+    [Arguments]    ${distance: int}    ${direction: Literal["UP", "DOWN"]}    ${slow: bool}=True
+    No Operation
+
+Move ${distance: int | float} meters
+    No Operation
diff --git a/tests/linter/rules/arguments/unused_argument/variable_type_conversion_expected.txt b/tests/linter/rules/arguments/unused_argument/variable_type_conversion_expected.txt
new file mode 100644
index 0000000..49df2ed
--- /dev/null
+++ b/tests/linter/rules/arguments/unused_argument/variable_type_conversion_expected.txt
@@ -0,0 +1,4 @@
+variable_type_conversion.robot:3:20 [W] ARG01 Keyword argument '${distance: int}' is not used
+variable_type_conversion.robot:3:40 [W] ARG01 Keyword argument '${direction: Literal["UP", "DOWN"]}' is not used
+variable_type_conversion.robot:3:79 [W] ARG01 Keyword argument '${slow: bool}' is not used
+variable_type_conversion.robot:6:6 [W] ARG01 Keyword argument '${distance: int | float}' is not used
\ No newline at end of file
diff --git a/tests/linter/rules/misc/inconsistent_assignment/test_rule.py b/tests/linter/rules/misc/inconsistent_assignment/test_rule.py
index 10d777a..fba6759 100644
--- a/tests/linter/rules/misc/inconsistent_assignment/test_rule.py
+++ b/tests/linter/rules/misc/inconsistent_assignment/test_rule.py
@@ -27,3 +27,10 @@ class TestRuleAcceptance(RuleAcceptance):
 
     def test_item_assignment(self):
         self.check_rule(src_files=["item_assignment.robot"], expected_file=None)
+
+    def test_variable_type_conversion(self):
+        self.check_rule(
+            src_files=["variable_type_conversion.robot"],
+            expected_file="variable_type_conversion_expected.txt",
+            test_on_version=">7.2.1",
+        )
diff --git a/tests/linter/rules/misc/inconsistent_assignment/variable_type_conversion.robot b/tests/linter/rules/misc/inconsistent_assignment/variable_type_conversion.robot
new file mode 100644
index 0000000..4fee304
--- /dev/null
+++ b/tests/linter/rules/misc/inconsistent_assignment/variable_type_conversion.robot
@@ -0,0 +1,19 @@
+*** Test Cases ***
+Assignment
+    [Tags]    robot:continue-on-failure
+    # In simple cases the VAR syntax is more convenient.
+    ${number: int}    Set Variable    42
+    Should Be Equal    ${number}    ${42}
+
+    # we can also convert other keywords output
+    ${number: str}    Convert To Integer    42
+    Should Be Equal    ${number}    ${42}
+
+    # or even convert to list
+    ${variable: list[str]}    Set Variable    42    43
+    Should Be Equal    ${variable}    ${42}
+
+    # In this example conversion is more useful.
+    ${match}    ${version: float} =    Should Match Regexp    RF 7.3    ^RF (\\d+\\.\\d+)$
+    Should Be Equal    ${match}      RF 7.3
+    Should Be Equal    ${version}    ${7.3}
diff --git a/tests/linter/rules/misc/inconsistent_assignment/variable_type_conversion_expected.txt b/tests/linter/rules/misc/inconsistent_assignment/variable_type_conversion_expected.txt
new file mode 100644
index 0000000..41c7341
--- /dev/null
+++ b/tests/linter/rules/misc/inconsistent_assignment/variable_type_conversion_expected.txt
@@ -0,0 +1,1 @@
+variable_type_conversion.robot:17:17 [W] MISC04 The assignment sign is not consistent within the file. Expected '' but got ' =' instead
\ No newline at end of file
diff --git a/tests/linter/rules/misc/inconsistent_assignment_in_variables/test_rule.py b/tests/linter/rules/misc/inconsistent_assignment_in_variables/test_rule.py
index dd9cdd5..4cb2284 100644
--- a/tests/linter/rules/misc/inconsistent_assignment_in_variables/test_rule.py
+++ b/tests/linter/rules/misc/inconsistent_assignment_in_variables/test_rule.py
@@ -24,3 +24,10 @@ class TestRuleAcceptance(RuleAcceptance):
             src_files=["autodetect/test.robot"],
             expected_file="autodetect/expected_output.txt",
         )
+
+    def test_variable_type_conversion(self):
+        self.check_rule(
+            src_files=["variable_type_conversion.robot"],
+            expected_file="variable_type_conversion_expected.txt",
+            test_on_version=">7.2.1",
+        )
diff --git a/tests/linter/rules/misc/inconsistent_assignment_in_variables/variable_type_conversion.robot b/tests/linter/rules/misc/inconsistent_assignment_in_variables/variable_type_conversion.robot
new file mode 100644
index 0000000..b14e757
--- /dev/null
+++ b/tests/linter/rules/misc/inconsistent_assignment_in_variables/variable_type_conversion.robot
@@ -0,0 +1,12 @@
+*** Variables ***
+# Simple type.
+${version: float}         7.3
+# Parameterized type.
+${CRITICAL: list[int]}    [3278, 5368, 5417]
+# With @{list} variables the type specified the item type.
+@{HIGH: int} =              4173    5334    5386    5387
+@{HIGH low: int}              4173    5334    5386    5387
+# With @{dict} variables the type specified the value type.
+&{DATes: date}            rc1=2025-05-08    final=2025-05-15
+# Alternative syntax to specify both key and value types.
+&{NUMBERS: int=float}     1=2.3    4=5.6
diff --git a/tests/linter/rules/misc/inconsistent_assignment_in_variables/variable_type_conversion_expected.txt b/tests/linter/rules/misc/inconsistent_assignment_in_variables/variable_type_conversion_expected.txt
new file mode 100644
index 0000000..d1856dd
--- /dev/null
+++ b/tests/linter/rules/misc/inconsistent_assignment_in_variables/variable_type_conversion_expected.txt
@@ -0,0 +1,1 @@
+variable_type_conversion.robot:7:1 [W] MISC05 The assignment sign is not consistent inside the variables section. Expected '' but got ' =' instead
\ No newline at end of file
diff --git a/tests/linter/rules/naming/section_variable_not_uppercase/test_rule.py b/tests/linter/rules/naming/section_variable_not_uppercase/test_rule.py
index ecefde9..cbfc067 100644
--- a/tests/linter/rules/naming/section_variable_not_uppercase/test_rule.py
+++ b/tests/linter/rules/naming/section_variable_not_uppercase/test_rule.py
@@ -16,3 +16,10 @@ class TestRuleAcceptance(RuleAcceptance):
     def test_rule_rf3(self):
         # RF3 doesn't recognize nested variables in Variables section
         self.check_rule(src_files=["test.robot"], expected_file="expected_output_rf3.txt", test_on_version="==3.*")
+
+    def test_variable_type_conversion(self):
+        self.check_rule(
+            src_files=["variable_type_conversion.robot"],
+            expected_file="variable_type_conversion_expected.txt",
+            test_on_version=">7.2.1",
+        )
diff --git a/tests/linter/rules/naming/section_variable_not_uppercase/variable_type_conversion.robot b/tests/linter/rules/naming/section_variable_not_uppercase/variable_type_conversion.robot
new file mode 100644
index 0000000..8006050
--- /dev/null
+++ b/tests/linter/rules/naming/section_variable_not_uppercase/variable_type_conversion.robot
@@ -0,0 +1,12 @@
+*** Variables ***
+# Simple type.
+${version: float}         7.3
+# Parameterized type.
+${CRITICAL: list[int]}    [3278, 5368, 5417]
+# With @{list} variables the type specified the item type.
+@{HIGH: int}              4173    5334    5386    5387
+@{HIGH low: int}              4173    5334    5386    5387
+# With @{dict} variables the type specified the value type.
+&{DATes: date}            rc1=2025-05-08    final=2025-05-15
+# Alternative syntax to specify both key and value types.
+&{NUMBERS: int=float}     1=2.3    4=5.6
diff --git a/tests/linter/rules/naming/section_variable_not_uppercase/variable_type_conversion_expected.txt b/tests/linter/rules/naming/section_variable_not_uppercase/variable_type_conversion_expected.txt
new file mode 100644
index 0000000..cf4d609
--- /dev/null
+++ b/tests/linter/rules/naming/section_variable_not_uppercase/variable_type_conversion_expected.txt
@@ -0,0 +1,3 @@
+variable_type_conversion.robot:3:1 [W] NAME08 Section variable '${version: float}' name is not uppercase
+variable_type_conversion.robot:8:1 [W] NAME08 Section variable '@{HIGH low: int}' name is not uppercase
+variable_type_conversion.robot:10:1 [W] NAME08 Section variable '&{DATes: date}' name is not uppercase
\ No newline at end of file
diff --git a/tests/linter/rules/variables/unused_variable/test_rule.py b/tests/linter/rules/variables/unused_variable/test_rule.py
index b09fd07..fa5c6cd 100644
--- a/tests/linter/rules/variables/unused_variable/test_rule.py
+++ b/tests/linter/rules/variables/unused_variable/test_rule.py
@@ -42,3 +42,10 @@ class TestRuleAcceptance(RuleAcceptance):
             src_files=["extended_variable_syntax.robot"],
             expected_file=None,
         )
+
+    def test_variable_type_conversion(self):
+        self.check_rule(
+            src_files=["variable_type_conversion.robot"],
+            expected_file="variable_type_conversion_expected.txt",
+            # test_on_version=">=7.3" FIXME
+        )
diff --git a/tests/linter/rules/variables/unused_variable/variable_type_conversion.robot b/tests/linter/rules/variables/unused_variable/variable_type_conversion.robot
new file mode 100644
index 0000000..57bd5c7
--- /dev/null
+++ b/tests/linter/rules/variables/unused_variable/variable_type_conversion.robot
@@ -0,0 +1,80 @@
+*** Variables ***
+# Simple type.
+${version: float}         7.3
+# Parameterized type.
+${CRITICAL: list[int]}    [3278, 5368, 5417]
+# With @{list} variables the type specified the item type.
+@{HIGH: int}              4173    5334    5386    5387
+@{HIGH low: int}              4173    5334    5386    5387
+# With @{dict} variables the type specified the value type.
+&{DATes: date}            rc1=2025-05-08    final=2025-05-15
+# Alternative syntax to specify both key and value types.
+&{NUMBERS: int=float}     1=2.3    4=5.6
+
+
+*** Test Cases ***
+Variables section
+    # Validate above variables using the inline Python evaluation syntax.
+    # This syntax is much more complicated than the syntax used above!
+    Should Be Equal    ${VERSION}       ${{7.3}}
+    Should Be Equal    ${CRITICAL}      ${{[3278, 5368, 5417]}}
+    Should Be Equal    ${HIGH}          ${{[4173, 5334, 5386, 5387]}}
+    Should Be Equal    ${DATES}         ${{{'rc1': datetime.date(2025, 5, 8), 'final': datetime.date(2025, 5, 15)}}}
+    Should Be Equal    ${NUMBERS}       ${{{1: 2.3, 4: 5.6}}}
+
+VAR syntax
+    # The VAR syntax supports types the same way as the Variables section
+    VAR    ${number: int}      42
+    Should Be Equal    ${number}    ${42}
+
+Assignment
+    [Tags]    robot:continue-on-failure
+    # In simple cases the VAR syntax is more convenient.
+    ${number: int} =    Set Variable    42
+    Should Be Equal    ${number}    ${42}
+
+    # we can also convert other keywords output
+    ${number: str}    Convert To Integer    42
+    Should Be Equal    ${number}    ${42}
+
+    # or even convert to list
+    ${variable: list[str]}    Set Variable    42    43
+    Should Be Equal    ${variable}    ${42}
+
+    # In this example conversion is more useful.
+    ${match}    ${version: float} =    Should Match Regexp    RF 7.3    ^RF (\\d+\\.\\d+)$
+    Should Be Equal    ${match}      RF 7.3
+    Should Be Equal    ${version}    ${7.3}
+
+Keyword arguments
+    # Argument conversion with user keywords is very convenient!
+    Move    10    down    slow=no
+    # Conversion handles validation automatically. This usage fails.
+    Move    10    invalid
+
+Embedded argumemts
+    # Also embedded arguments can be converted.
+    Move 3.14 meters
+
+Other
+    Blocks
+
+*** Keywords ***
+Move
+    [Arguments]    ${distance: int}    ${direction: Literal["UP", "DOWN"]}    ${slow: bool}=True
+    Should Be Equal    ${distance}     ${10}
+    Should Be Equal    ${direction}    DOWN
+    Should Be Equal    ${slow}         ${False}
+
+Move ${distance: int | float} meters
+    Should Be Equal    ${distance}     ${3.14}
+
+Return Variable
+    [Arguments]    ${variable}
+    RETURN    ${variable}
+
+Blocks
+    # 'var: int' will be unused, since FOR does not support var type conversion
+    FOR    ${var: int}    IN    @{list_of_strings}
+        Type Should Be    ${var}    int
+    END
diff --git a/tests/linter/rules/variables/unused_variable/variable_type_conversion_expected.txt b/tests/linter/rules/variables/unused_variable/variable_type_conversion_expected.txt
new file mode 100644
index 0000000..b05f738
--- /dev/null
+++ b/tests/linter/rules/variables/unused_variable/variable_type_conversion_expected.txt
@@ -0,0 +1,2 @@
+variable_type_conversion.robot:8:1 [I] VAR02 Variable '@{HIGH low: int}' is assigned but not used
+variable_type_conversion.robot:78:12 [I] VAR02 Variable '${var: int}' is assigned but not used
\ No newline at end of file
diff --git a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_extended.txt b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_extended.txt
index 0dd6c81..ee1e6e0 100644
--- a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_extended.txt
+++ b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_extended.txt
@@ -7,12 +7,12 @@ test.robot:7:5 VAR03 Local variable '${value}' is overwritten before usage
  8 |     ${value}    Keyword
    |
 
-test.robot:15:5 VAR03 Local variable '${val_ue}' is overwritten before usage
+test.robot:15:5 VAR03 Local variable '${value}' is overwritten before usage
     |
  13 |
  14 | Overwritten IF assign
  15 |     ${value} =    IF    $condition    Keyword
-    |     ^^^^^^^^^ VAR03
+    |     ^^^^^^^^ VAR03
  16 |     ${val_ue}    Keyword
     |
 
diff --git a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_after_var.txt b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_after_var.txt
index bd38a41..ec1d3c0 100644
--- a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_after_var.txt
+++ b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_after_var.txt
@@ -1,5 +1,5 @@
 test.robot:7:5 [W] VAR03 Local variable '${value}' is overwritten before usage
-test.robot:15:5 [W] VAR03 Local variable '${val_ue}' is overwritten before usage
+test.robot:15:5 [W] VAR03 Local variable '${value}' is overwritten before usage
 test.robot:121:12 [W] VAR03 Local variable '${variable}' is overwritten before usage
 test.robot:123:12 [W] VAR03 Local variable '${variable2}' is overwritten before usage
 test.robot:127:5 [W] VAR03 Local variable '${from_keyword3}' is overwritten before usage
diff --git a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_loops.txt b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_loops.txt
index ae3145e..dcd3837 100644
--- a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_loops.txt
+++ b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_loops.txt
@@ -1,1 +1,1 @@
-loops.robot:43:9:43:20 [W] VAR03 Local variable '${variable}' is overwritten before usage
\ No newline at end of file
+loops.robot:43:9:43:20 [W] VAR03 Local variable '${variablE}' is overwritten before usage
\ No newline at end of file
diff --git a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_pre_var.txt b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_pre_var.txt
index 5922899..d2cb3e2 100644
--- a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_pre_var.txt
+++ b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_pre_var.txt
@@ -1,2 +1,2 @@
 test.robot:7:5 [W] VAR03 Local variable '${value}' is overwritten before usage
-test.robot:15:5 [W] VAR03 Local variable '${val_ue}' is overwritten before usage
\ No newline at end of file
+test.robot:15:5 [W] VAR03 Local variable '${value}' is overwritten before usage
\ No newline at end of file
diff --git a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_rf3.txt b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_rf3.txt
index 428b448..be94da0 100644
--- a/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_rf3.txt
+++ b/tests/linter/rules/variables/variable_overwritten_before_usage/expected_output_rf3.txt
@@ -1,5 +1,5 @@
 test.robot:7:5 [W] VAR03 Local variable '${value}' is overwritten before usage
-test.robot:15:5 [W] VAR03 Local variable '${val_ue}' is overwritten before usage
+test.robot:15:5 [W] VAR03 Local variable '${value}' is overwritten before usage
 test.robot:27:5 [W] VAR03 Local variable '${overwritten}' is overwritten before usage
 test.robot:29:9 [W] VAR03 Local variable '${longitude}' is overwritten before usage
 test.robot:113:5 [W] VAR03 Local variable '${variable}' is overwritten before usage
diff --git a/tests/linter/rules/variables/variable_overwritten_before_usage/test_rule.py b/tests/linter/rules/variables/variable_overwritten_before_usage/test_rule.py
index a89ae8e..0dcd4e7 100644
--- a/tests/linter/rules/variables/variable_overwritten_before_usage/test_rule.py
+++ b/tests/linter/rules/variables/variable_overwritten_before_usage/test_rule.py
@@ -29,3 +29,10 @@ class TestRuleAcceptance(RuleAcceptance):
 
     def test_rule_inline_if(self):
         self.check_rule(src_files=["inline_if.robot"], expected_file=None, test_on_version=">=4")
+
+    def test_variable_type_conversion(self):
+        self.check_rule(
+            src_files=["variable_type_conversion.robot"],
+            expected_file="variable_type_conversion_expected.txt",
+            # test_on_version=">=7.3r" FIXME
+        )
diff --git a/tests/linter/rules/variables/variable_overwritten_before_usage/variable_type_conversion.robot b/tests/linter/rules/variables/variable_overwritten_before_usage/variable_type_conversion.robot
new file mode 100644
index 0000000..72abe30
--- /dev/null
+++ b/tests/linter/rules/variables/variable_overwritten_before_usage/variable_type_conversion.robot
@@ -0,0 +1,68 @@
+*** Variables ***
+# Simple type.
+${version: float}         7.3
+# Parameterized type.
+${CRITICAL: list[int]}    [3278, 5368, 5417]
+# With @{list} variables the type specified the item type.
+@{HIGH: int}              4173    5334    5386    5387
+@{HIGH low: int}              4173    5334    5386    5387
+# With @{dict} variables the type specified the value type.
+&{DATes: date}            rc1=2025-05-08    final=2025-05-15
+# Alternative syntax to specify both key and value types.
+&{NUMBERS: int=float}     1=2.3    4=5.6
+
+
+*** Test Cases ***
+VAR syntax
+    # The VAR syntax supports types the same way as the Variables section
+    VAR    ${number: int}      42
+    VAR    ${number2: int}      42
+    VAR    ${number}      42
+    Log    ${number2}
+    VAR    ${number2}      42
+
+Assignment
+    [Tags]    robot:continue-on-failure
+    # In simple cases the VAR syntax is more convenient.
+    ${number: int} =    Set Variable    42
+    ${number: str}    Convert To Integer    42
+
+    # or even convert to list
+    ${variable: list[str]}    Set Variable    42    43
+    Should Be Equal    ${variable}    ${42}
+
+    # In this example conversion is more useful.
+    ${match}    ${number} =    Should Match Regexp    RF 7.3    ^RF (\\d+\\.\\d+)$
+
+Keyword arguments
+    # Argument conversion with user keywords is very convenient!
+    Move    10    down    slow=no
+    # Conversion handles validation automatically. This usage fails.
+    Move    10    invalid
+
+Embedded argumemts
+    # Also embedded arguments can be converted.
+    Move 3.14 meters
+
+Other
+    Blocks
+
+*** Keywords ***
+Move
+    [Arguments]    ${distance: int}    ${direction: Literal["UP", "DOWN"]}    ${slow: bool}=True
+    Should Be Equal    ${distance}     ${10}
+    Should Be Equal    ${direction}    DOWN
+    Should Be Equal    ${slow}         ${False}
+
+Move ${distance: int | float} meters
+    Should Be Equal    ${distance}     ${3.14}
+
+Return Variable
+    [Arguments]    ${variable}
+    RETURN    ${variable}
+
+Blocks
+    # 'var: int' will be unused, since FOR does not support var type conversion
+    FOR    ${var: int}    IN    @{list_of_strings}
+        Type Should Be    ${var}    int
+    END
diff --git a/tests/linter/rules/variables/variable_overwritten_before_usage/variable_type_conversion_expected.txt b/tests/linter/rules/variables/variable_overwritten_before_usage/variable_type_conversion_expected.txt
new file mode 100644
index 0000000..3e8e3ed
--- /dev/null
+++ b/tests/linter/rules/variables/variable_overwritten_before_usage/variable_type_conversion_expected.txt
@@ -0,0 +1,3 @@
+variable_type_conversion.robot:18:12 [W] VAR03 Local variable '${number: int}' is overwritten before usage
+variable_type_conversion.robot:27:5 [W] VAR03 Local variable '${number: int}' is overwritten before usage
+variable_type_conversion.robot:28:5 [W] VAR03 Local variable '${number: str}' is overwritten before usage
\ No newline at end of file