Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

---

## [0.2.0] - 2025-08-23

### Added
- XML launch file parsing support
- Autoware XML Launch Real Test File

### Changed
- Reorganized the parser contents for demarcation between python and xml with common utilities

## [0.1.10] - 2025-08-20

### Added
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "launchmap",
"displayName": "LaunchMap",
"description": "Visualize ROS2 Launch Files",
"version": "0.1.10",
"version": "0.2.0",
"publisher": "kodorobotics",
"icon": "assets/launchmap-logo.png",
"bugs": {
Expand Down
2 changes: 1 addition & 1 deletion parser/custom_handlers/rewritten_yaml_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

from parser.parser.postprocessing import simplify_launch_configurations
from parser.parser.user_handler import register_user_handler
from parser.parser.python.user_handler import register_user_handler
from parser.resolution.utils import resolve_call_kwargs


Expand Down
45 changes: 45 additions & 0 deletions parser/entrypoint/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (c) 2025 Kodo Robotics
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List

from parser.context import ParseContext


def build_result(filepath: str, context: ParseContext, parsed: List):
"""
Shape the final response uniformly.
"""
return {
"file": filepath,
"parsed": parsed,
"used_launch_config": sorted(context.introspection.used_launch_configs),
"declared_arguments": sorted(context.introspection.declared_launch_args.keys()),
"undeclared_launch_configurations": sorted(
context.introspection.get_undeclared_launch_configs()
),
"environment_variables": context.introspection.get_environment_variables(),
"python_expressions": context.introspection.get_python_expressions(),
"composable_containers": context.get_composable_node_groups(),
"additional_components": context.introspection.get_registered_entities(),
}

def detect_format_from_content(code: str) -> str:
"""
Return 'xml' if it parses as XML with <launch> root; otherwise 'python'.
"""
head = code.lstrip().lower()
if head.startswith("<?xml") or head.startswith("<launch"):
return "xml"
return "python"
86 changes: 7 additions & 79 deletions parser/entrypoint/parser_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,89 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import ast

from parser.context import ParseContext
from parser.parser.utils.ast_utils import collect_function_defs
from parser.resolution.resolution_engine import ResolutionEngine
from parser.entrypoint.common import detect_format_from_content
from parser.entrypoint.python_runner import parse_python_launch_file
from parser.entrypoint.xml_runner import parse_xml_launch_file


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

tree = ast.parse(code, filename=filepath)

# Set up shared context and resolution engine
context = ParseContext()
context.current_file = filepath
engine = ResolutionEngine(context)
context.engine = engine

parsed = []

collect_function_defs(tree.body, context)

# Simulate top-level execution
for node in tree.body:
if isinstance(node, ast.Assign):
engine.resolve(node)

elif isinstance(node, ast.Expr):
engine.resolve(node)

# Now extract and run generate_launch_description
main_fn = context.lookup_function("generate_launch_description")
if not main_fn:
raise ValueError("No generate_launch_description() function found.")

parsed.extend(_parse_launch_function_body(main_fn.body, context, engine))

return {
"file": filepath,
"parsed": parsed,
"used_launch_config": sorted(context.introspection.used_launch_configs),
"declared_arguments": sorted(context.introspection.declared_launch_args.keys()),
"undeclared_launch_configurations": sorted(
context.introspection.get_undeclared_launch_configs()
),
"environment_variables": context.introspection.get_environment_variables(),
"python_expressions": context.introspection.get_python_expressions(),
"composable_containers": context.get_composable_node_groups(),
"additional_components": context.introspection.get_registered_entities(),
}


def _parse_launch_function_body(
body: list[ast.stmt], context: ParseContext, engine: ResolutionEngine
) -> list:
parsed = []
for stmt in body:
if isinstance(stmt, ast.Assign):
engine.resolve(stmt)

elif isinstance(stmt, ast.If):
engine.resolve(stmt)

elif isinstance(stmt, ast.Expr):
func_name = context.get_func_name(stmt.value.func)
if func_name.endswith("add_action"):
arg = stmt.value.args[0]
result = engine.resolve(arg)
if result:
parsed.append(result)
else:
engine.resolve(stmt)

elif isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Call):
resolved = engine.resolve(stmt.value)
if isinstance(resolved, list):
parsed.extend(resolved)
elif resolved is not None:
parsed.append(resolved)
kind = detect_format_from_content(code)

return parsed
if kind == "xml":
return parse_xml_launch_file(filepath)
return parse_python_launch_file(filepath)
89 changes: 89 additions & 0 deletions parser/entrypoint/python_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright (c) 2025 Kodo Robotics
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import ast

from parser.context import ParseContext
from parser.entrypoint.common import build_result
from parser.parser.python.utils import collect_function_defs
from parser.resolution.resolution_engine import ResolutionEngine


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

tree = ast.parse(code, filename=filepath)

# Set up shared context and resolution engine
context = ParseContext()
context.current_file = filepath
engine = ResolutionEngine(context)
context.engine = engine

parsed = []

collect_function_defs(tree.body, context)

# Simulate top-level execution
for node in tree.body:
if isinstance(node, ast.Assign):
engine.resolve(node)

elif isinstance(node, ast.Expr):
engine.resolve(node)

# Now extract and run generate_launch_description
main_fn = context.lookup_function("generate_launch_description")
if not main_fn:
raise ValueError("No generate_launch_description() function found.")

parsed.extend(_parse_launch_function_body(main_fn.body, context, engine))

return build_result(filepath, context, parsed)


def _parse_launch_function_body(
body: list[ast.stmt], context: ParseContext, engine: ResolutionEngine
) -> list:
parsed = []
for stmt in body:
if isinstance(stmt, ast.Assign):
engine.resolve(stmt)

elif isinstance(stmt, ast.If):
engine.resolve(stmt)

elif isinstance(stmt, ast.Expr):
func_name = context.get_func_name(stmt.value.func)
if func_name.endswith("add_action"):
arg = stmt.value.args[0]
result = engine.resolve(arg)
if result:
parsed.append(result)
else:
engine.resolve(stmt)

elif isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Call):
resolved = engine.resolve(stmt.value)
if isinstance(resolved, list):
parsed.extend(resolved)
elif resolved is not None:
parsed.append(resolved)

return parsed
2 changes: 1 addition & 1 deletion parser/entrypoint/user_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
collect_python_variable_usages,
)
from parser.parser.postprocessing import simplify_launch_configurations
from parser.parser.utils.common import group_entities_by_type
from parser.parser.utils import group_entities_by_type


def parse_and_format_launch_file(filepath: str) -> dict:
Expand Down
42 changes: 42 additions & 0 deletions parser/entrypoint/xml_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright (c) 2025 Kodo Robotics
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from xml.etree import ElementTree as ET

from parser.context import ParseContext
from parser.entrypoint.common import build_result
from parser.parser.xml.dispatcher import dispatch_element
from parser.parser.xml.utils import strip_ns


def parse_xml_launch_file(filepath: str) -> dict:
"""
Entrypoint: parses a launch file and returns structured output
Detects <launch>
"""
root = ET.parse(filepath).getroot()
tag = strip_ns(root.tag)
if tag != "launch":
raise ValueError(f"Expected <launch> as root tag, found <{tag}")

# Set up shared context
context = ParseContext()
context.current_file = filepath

parsed = []
for child in list(root):
result = dispatch_element(child, context)
parsed.append(result)

return build_result(filepath, context, parsed)
2 changes: 1 addition & 1 deletion parser/introspection/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from parser.parser.utils.common import compute_entity_key
from parser.parser.utils import compute_entity_key


class IntrospectionTracker:
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import ast

from parser.context import ParseContext
from parser.parser.loader import register_builtin_handlers
from parser.parser.python.loader import register_builtin_handlers
from parser.parser.registry import get_handler
from parser.resolution.utils import get_func_name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from parser.context import ParseContext
from parser.parser.registry import register_handler
from parser.parser.utils.common import flatten_once, group_entities_by_type
from parser.parser.utils import flatten_once, group_entities_by_type
from parser.resolution.utils import resolve_call_signature


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from parser.context import ParseContext
from parser.parser.registry import register_handler
from parser.parser.utils.common import flatten_once, group_entities_by_type
from parser.parser.utils import flatten_once, group_entities_by_type
from parser.resolution.utils import resolve_call_signature


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from parser.context import ParseContext
from parser.parser.registry import register_handler
from parser.parser.utils.common import flatten_once
from parser.parser.utils import flatten_once
from parser.resolution.utils import resolve_call_signature


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from parser.context import ParseContext
from parser.parser.postprocessing import simplify_launch_configurations
from parser.parser.registry import register_handler
from parser.parser.utils.common import flatten_once, group_entities_by_type
from parser.parser.utils import flatten_once, group_entities_by_type
from parser.resolution.utils import resolve_call_signature


Expand Down
Loading