Skip to content

Commit 0d3233b

Browse files
authored
Merge pull request #61 from Kodo-Robotics/xml
Add parser support for XML Launch Files
2 parents 80ef46c + a642905 commit 0d3233b

92 files changed

Lines changed: 2092 additions & 148 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
77

88
---
99

10+
## [0.2.0] - 2025-08-23
11+
12+
### Added
13+
- XML launch file parsing support
14+
- Autoware XML Launch Real Test File
15+
16+
### Changed
17+
- Reorganized the parser contents for demarcation between python and xml with common utilities
18+
1019
## [0.1.10] - 2025-08-20
1120

1221
### Added

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "launchmap",
33
"displayName": "LaunchMap",
44
"description": "Visualize ROS2 Launch Files",
5-
"version": "0.1.10",
5+
"version": "0.2.0",
66
"publisher": "kodorobotics",
77
"icon": "assets/launchmap-logo.png",
88
"bugs": {

parser/custom_handlers/rewritten_yaml_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
from parser.parser.postprocessing import simplify_launch_configurations
16-
from parser.parser.user_handler import register_user_handler
16+
from parser.parser.python.user_handler import register_user_handler
1717
from parser.resolution.utils import resolve_call_kwargs
1818

1919

parser/entrypoint/common.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright (c) 2025 Kodo Robotics
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import List
16+
17+
from parser.context import ParseContext
18+
19+
20+
def build_result(filepath: str, context: ParseContext, parsed: List):
21+
"""
22+
Shape the final response uniformly.
23+
"""
24+
return {
25+
"file": filepath,
26+
"parsed": parsed,
27+
"used_launch_config": sorted(context.introspection.used_launch_configs),
28+
"declared_arguments": sorted(context.introspection.declared_launch_args.keys()),
29+
"undeclared_launch_configurations": sorted(
30+
context.introspection.get_undeclared_launch_configs()
31+
),
32+
"environment_variables": context.introspection.get_environment_variables(),
33+
"python_expressions": context.introspection.get_python_expressions(),
34+
"composable_containers": context.get_composable_node_groups(),
35+
"additional_components": context.introspection.get_registered_entities(),
36+
}
37+
38+
def detect_format_from_content(code: str) -> str:
39+
"""
40+
Return 'xml' if it parses as XML with <launch> root; otherwise 'python'.
41+
"""
42+
head = code.lstrip().lower()
43+
if head.startswith("<?xml") or head.startswith("<launch"):
44+
return "xml"
45+
return "python"

parser/entrypoint/parser_runner.py

Lines changed: 7 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -12,89 +12,17 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import ast
16-
17-
from parser.context import ParseContext
18-
from parser.parser.utils.ast_utils import collect_function_defs
19-
from parser.resolution.resolution_engine import ResolutionEngine
15+
from parser.entrypoint.common import detect_format_from_content
16+
from parser.entrypoint.python_runner import parse_python_launch_file
17+
from parser.entrypoint.xml_runner import parse_xml_launch_file
2018

2119

2220
def parse_launch_file(filepath: str) -> dict:
23-
"""
24-
Entrypoint: parses a launch file and returns structured output
25-
Detects LaunchDescription([...]) or ld.add_action(...) usage.
26-
"""
2721
with open(filepath, "r", encoding="utf-8") as f:
2822
code = f.read()
2923

30-
tree = ast.parse(code, filename=filepath)
31-
32-
# Set up shared context and resolution engine
33-
context = ParseContext()
34-
context.current_file = filepath
35-
engine = ResolutionEngine(context)
36-
context.engine = engine
37-
38-
parsed = []
39-
40-
collect_function_defs(tree.body, context)
41-
42-
# Simulate top-level execution
43-
for node in tree.body:
44-
if isinstance(node, ast.Assign):
45-
engine.resolve(node)
46-
47-
elif isinstance(node, ast.Expr):
48-
engine.resolve(node)
49-
50-
# Now extract and run generate_launch_description
51-
main_fn = context.lookup_function("generate_launch_description")
52-
if not main_fn:
53-
raise ValueError("No generate_launch_description() function found.")
54-
55-
parsed.extend(_parse_launch_function_body(main_fn.body, context, engine))
56-
57-
return {
58-
"file": filepath,
59-
"parsed": parsed,
60-
"used_launch_config": sorted(context.introspection.used_launch_configs),
61-
"declared_arguments": sorted(context.introspection.declared_launch_args.keys()),
62-
"undeclared_launch_configurations": sorted(
63-
context.introspection.get_undeclared_launch_configs()
64-
),
65-
"environment_variables": context.introspection.get_environment_variables(),
66-
"python_expressions": context.introspection.get_python_expressions(),
67-
"composable_containers": context.get_composable_node_groups(),
68-
"additional_components": context.introspection.get_registered_entities(),
69-
}
70-
71-
72-
def _parse_launch_function_body(
73-
body: list[ast.stmt], context: ParseContext, engine: ResolutionEngine
74-
) -> list:
75-
parsed = []
76-
for stmt in body:
77-
if isinstance(stmt, ast.Assign):
78-
engine.resolve(stmt)
79-
80-
elif isinstance(stmt, ast.If):
81-
engine.resolve(stmt)
82-
83-
elif isinstance(stmt, ast.Expr):
84-
func_name = context.get_func_name(stmt.value.func)
85-
if func_name.endswith("add_action"):
86-
arg = stmt.value.args[0]
87-
result = engine.resolve(arg)
88-
if result:
89-
parsed.append(result)
90-
else:
91-
engine.resolve(stmt)
92-
93-
elif isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Call):
94-
resolved = engine.resolve(stmt.value)
95-
if isinstance(resolved, list):
96-
parsed.extend(resolved)
97-
elif resolved is not None:
98-
parsed.append(resolved)
24+
kind = detect_format_from_content(code)
9925

100-
return parsed
26+
if kind == "xml":
27+
return parse_xml_launch_file(filepath)
28+
return parse_python_launch_file(filepath)

parser/entrypoint/python_runner.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright (c) 2025 Kodo Robotics
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import ast
16+
17+
from parser.context import ParseContext
18+
from parser.entrypoint.common import build_result
19+
from parser.parser.python.utils import collect_function_defs
20+
from parser.resolution.resolution_engine import ResolutionEngine
21+
22+
23+
def parse_python_launch_file(filepath: str) -> dict:
24+
"""
25+
Entrypoint: parses a launch file and returns structured output
26+
Detects LaunchDescription([...]) or ld.add_action(...) usage.
27+
"""
28+
with open(filepath, "r", encoding="utf-8") as f:
29+
code = f.read()
30+
31+
tree = ast.parse(code, filename=filepath)
32+
33+
# Set up shared context and resolution engine
34+
context = ParseContext()
35+
context.current_file = filepath
36+
engine = ResolutionEngine(context)
37+
context.engine = engine
38+
39+
parsed = []
40+
41+
collect_function_defs(tree.body, context)
42+
43+
# Simulate top-level execution
44+
for node in tree.body:
45+
if isinstance(node, ast.Assign):
46+
engine.resolve(node)
47+
48+
elif isinstance(node, ast.Expr):
49+
engine.resolve(node)
50+
51+
# Now extract and run generate_launch_description
52+
main_fn = context.lookup_function("generate_launch_description")
53+
if not main_fn:
54+
raise ValueError("No generate_launch_description() function found.")
55+
56+
parsed.extend(_parse_launch_function_body(main_fn.body, context, engine))
57+
58+
return build_result(filepath, context, parsed)
59+
60+
61+
def _parse_launch_function_body(
62+
body: list[ast.stmt], context: ParseContext, engine: ResolutionEngine
63+
) -> list:
64+
parsed = []
65+
for stmt in body:
66+
if isinstance(stmt, ast.Assign):
67+
engine.resolve(stmt)
68+
69+
elif isinstance(stmt, ast.If):
70+
engine.resolve(stmt)
71+
72+
elif isinstance(stmt, ast.Expr):
73+
func_name = context.get_func_name(stmt.value.func)
74+
if func_name.endswith("add_action"):
75+
arg = stmt.value.args[0]
76+
result = engine.resolve(arg)
77+
if result:
78+
parsed.append(result)
79+
else:
80+
engine.resolve(stmt)
81+
82+
elif isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Call):
83+
resolved = engine.resolve(stmt.value)
84+
if isinstance(resolved, list):
85+
parsed.extend(resolved)
86+
elif resolved is not None:
87+
parsed.append(resolved)
88+
89+
return parsed

parser/entrypoint/user_interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
collect_python_variable_usages,
2222
)
2323
from parser.parser.postprocessing import simplify_launch_configurations
24-
from parser.parser.utils.common import group_entities_by_type
24+
from parser.parser.utils import group_entities_by_type
2525

2626

2727
def parse_and_format_launch_file(filepath: str) -> dict:

parser/entrypoint/xml_runner.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright (c) 2025 Kodo Robotics
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from xml.etree import ElementTree as ET
16+
17+
from parser.context import ParseContext
18+
from parser.entrypoint.common import build_result
19+
from parser.parser.xml.dispatcher import dispatch_element
20+
from parser.parser.xml.utils import strip_ns
21+
22+
23+
def parse_xml_launch_file(filepath: str) -> dict:
24+
"""
25+
Entrypoint: parses a launch file and returns structured output
26+
Detects <launch>
27+
"""
28+
root = ET.parse(filepath).getroot()
29+
tag = strip_ns(root.tag)
30+
if tag != "launch":
31+
raise ValueError(f"Expected <launch> as root tag, found <{tag}")
32+
33+
# Set up shared context
34+
context = ParseContext()
35+
context.current_file = filepath
36+
37+
parsed = []
38+
for child in list(root):
39+
result = dispatch_element(child, context)
40+
parsed.append(result)
41+
42+
return build_result(filepath, context, parsed)

parser/introspection/tracker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from parser.parser.utils.common import compute_entity_key
15+
from parser.parser.utils import compute_entity_key
1616

1717

1818
class IntrospectionTracker:

0 commit comments

Comments
 (0)