Skip to content

Commit b2d9668

Browse files
authored
ignore commented launch dependencies (#13)
1 parent ae5d26d commit b2d9668

6 files changed

Lines changed: 356 additions & 7 deletions

File tree

package_xml_validation/helpers/find_launch_dependencies.py

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
55
Recursively search a ROS 2 package's launch/ folder and extract
66
all referenced ROS 2 package names via a small set of regexes.
7+
8+
Now ignores matches that occur inside comments:
9+
- Python: # line comments, and triple-quoted blocks
10+
- XML/.launch: <!-- ... --> comments
11+
- YAML: # line comments
712
"""
813

914
import os
@@ -29,21 +34,111 @@
2934
r"\$\(\s*find-pkg-share\s+([A-Za-z0-9_]+)\s*\)",
3035
]
3136

32-
# Compile once for speed
3337
COMPILED = [re.compile(rx) for rx in REGEX_EXPR]
3438

39+
_TRIPLE_QUOTE_BLOCK = re.compile(r"(?s)(['\"]{3})(?:.*?)(\1)")
40+
_XML_COMMENT_BLOCK = re.compile(r"(?s)<!--.*?-->")
41+
42+
43+
def _strip_hash_line_comments_outside_strings(text: str) -> str:
44+
"""
45+
Remove '#' to end-of-line comments that occur OUTSIDE of single/double quoted strings.
46+
Preserves newlines.
47+
Suitable for Python and YAML after any triple-quoted removal (for Python).
48+
"""
49+
out = []
50+
in_single = False
51+
in_double = False
52+
i = 0
53+
n = len(text)
54+
55+
while i < n:
56+
ch = text[i]
57+
58+
# Handle escapes inside strings
59+
if ch == "\\" and (in_single or in_double) and i + 1 < n:
60+
out.append(ch)
61+
out.append(text[i + 1])
62+
i += 2
63+
continue
64+
65+
if not in_single and not in_double:
66+
if ch == "#":
67+
# skip until end of line (keep the newline itself)
68+
while i < n and text[i] not in ("\n", "\r"):
69+
i += 1
70+
# fall through to append the newline (if any)
71+
continue
72+
elif ch == "'":
73+
in_single = True
74+
out.append(ch)
75+
i += 1
76+
continue
77+
elif ch == '"':
78+
in_double = True
79+
out.append(ch)
80+
i += 1
81+
continue
82+
else:
83+
out.append(ch)
84+
i += 1
85+
continue
86+
else:
87+
# inside quotes
88+
if in_single and ch == "'":
89+
in_single = False
90+
elif in_double and ch == '"':
91+
in_double = False
92+
out.append(ch)
93+
i += 1
94+
95+
return "".join(out)
96+
3597

36-
def scan_file(path, found: set[str], verbose: bool = False):
37-
"""Apply every regex to the file and add matches to `found`."""
98+
def _decomment_python(text: str) -> str:
99+
# 1) drop triple-quoted blocks entirely
100+
text = _TRIPLE_QUOTE_BLOCK.sub("", text)
101+
# 2) drop '#' comments outside of quoted strings
102+
text = _strip_hash_line_comments_outside_strings(text)
103+
return text
104+
105+
106+
def _decomment_xml(text: str) -> str:
107+
# Drop <!-- ... --> blocks
108+
return _XML_COMMENT_BLOCK.sub("", text)
109+
110+
111+
def _decomment_yaml(text: str) -> str:
112+
# YAML has only '#' line comments; be string-aware for ' and "
113+
return _strip_hash_line_comments_outside_strings(text)
114+
115+
116+
def _decomment_for_suffix(suffix: str, text: str) -> str:
117+
s = suffix.lower()
118+
if s.endswith(".py"):
119+
return _decomment_python(text)
120+
if s.endswith(".xml") or s.endswith(".launch"):
121+
return _decomment_xml(text)
122+
if s.endswith(".yaml") or s.endswith(".yml"):
123+
return _decomment_yaml(text)
124+
# default: no decommenting
125+
return text
126+
127+
128+
def scan_file(path: str, found: set[str], verbose: bool = False):
129+
"""Apply every regex to the file after stripping comments; add matches to `found`."""
38130
with open(path, encoding="utf-8") as f:
39131
text = f.read()
40132

133+
text = _decomment_for_suffix(path, text)
134+
41135
for i, rx in enumerate(COMPILED):
42136
for m in rx.finditer(text):
43-
found.add(m.group(1))
137+
pkg = m.group(1)
138+
found.add(pkg)
44139
if verbose:
45140
print(
46-
f"Found package '{m.group(1)}' in {os.path.basename(path)} with regex {REGEX_EXPR[i]}"
141+
f"Found package '{pkg}' in {os.path.basename(path)} with regex {REGEX_EXPR[i]}"
47142
)
48143

49144

@@ -52,15 +147,16 @@ def scan_files(launch_dir: str, verbose: bool = False) -> list[str]:
52147
Extracts launch dependencies from the specified directory.
53148
Launch dependencies are listed packages names in the launch files.
54149
It uses regex to extract package names from common launch patterns.
150+
Comments are stripped (type-specific) before matching.
55151
"""
56152
if not os.path.isdir(launch_dir):
57153
print(f"Error: '{launch_dir}' is not a directory.")
58154
return []
59155

60-
pkgs = set()
156+
pkgs: set[str] = set()
61157

62158
for root, _, files in os.walk(launch_dir):
63159
for fn in files:
64160
if fn.endswith((".py", ".xml", ".yaml", ".launch", ".yml")):
65161
scan_file(os.path.join(root, fn), pkgs, verbose)
66-
return list(pkgs)
162+
return sorted(pkgs)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# description: Announcer for Athena Multi Robot
2+
# provides: announcement
3+
# launch:
4+
# package: athena_announcer_comment
5+
# executable: athena_announcer
6+
description: Announcer for Athena Multi Robot
7+
provides: announcement
8+
launch:
9+
package: athena_announcer
10+
executable: athena_announcer
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# from https://docs.ros.org/en/rolling/How-To-Guides/Launch-file-different-formats.html
2+
from launch import LaunchDescription
3+
from launch.actions import DeclareLaunchArgument, GroupAction, IncludeLaunchDescription
4+
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
5+
from launch_ros.actions import Node, PushROSNamespace
6+
from launch_ros.substitutions import FindPackageShare
7+
8+
9+
def generate_launch_description():
10+
# launch_dir = PathJoinSubstitution(
11+
# [FindPackageShare("demo_nodes_cpp_comment"), "launch", "topics"]
12+
# )
13+
"""
14+
launch_dir = PathJoinSubstitution(
15+
[FindPackageShare("demo_nodes_cpp_commentII"), "launch", "topics"]
16+
)
17+
"""
18+
launch_dir = PathJoinSubstitution(
19+
[FindPackageShare("demo_nodes_cpp"), "launch", "topics"]
20+
)
21+
return LaunchDescription(
22+
[
23+
# args that can be set from the command line or a default will be used
24+
DeclareLaunchArgument("background_r", default_value="0"),
25+
DeclareLaunchArgument("background_g", default_value="255"),
26+
DeclareLaunchArgument("background_b", default_value="0"),
27+
DeclareLaunchArgument("chatter_py_ns", default_value="chatter/py/ns"),
28+
DeclareLaunchArgument("chatter_xml_ns", default_value="chatter/xml/ns"),
29+
DeclareLaunchArgument("chatter_yaml_ns", default_value="chatter/yaml/ns"),
30+
# include another launch file
31+
IncludeLaunchDescription(
32+
PathJoinSubstitution([launch_dir, "talker_listener_launch.py"])
33+
),
34+
# include a Python launch file in the chatter_py_ns namespace
35+
GroupAction(
36+
actions=[
37+
# push_ros_namespace first to set namespace of included nodes for following actions
38+
PushROSNamespace("chatter_py_ns"),
39+
IncludeLaunchDescription(
40+
PathJoinSubstitution([launch_dir, "talker_listener_launch.py"])
41+
),
42+
]
43+
),
44+
# include a xml launch file in the chatter_xml_ns namespace
45+
GroupAction(
46+
actions=[
47+
# push_ros_namespace first to set namespace of included nodes for following actions
48+
PushROSNamespace("chatter_xml_ns"),
49+
IncludeLaunchDescription(
50+
PathJoinSubstitution([launch_dir, "talker_listener_launch.xml"])
51+
),
52+
]
53+
),
54+
# include a yaml launch file in the chatter_yaml_ns namespace
55+
GroupAction(
56+
actions=[
57+
# push_ros_namespace first to set namespace of included nodes for following actions
58+
PushROSNamespace("chatter_yaml_ns"),
59+
IncludeLaunchDescription(
60+
PathJoinSubstitution(
61+
[launch_dir, "talker_listener_launch.yaml"]
62+
)
63+
),
64+
]
65+
),
66+
# start a turtlesim_node in the turtlesim1 namespace
67+
Node(
68+
package="turtlesim",
69+
namespace="turtlesim1",
70+
executable="turtlesim_node",
71+
name="sim",
72+
),
73+
# start another turtlesim_node in the turtlesim2 namespace
74+
# and use args to set parameters
75+
Node(
76+
package="turtlesim",
77+
namespace="turtlesim2",
78+
executable="turtlesim_node",
79+
name="sim",
80+
parameters=[
81+
{
82+
"background_r": LaunchConfiguration("background_r"),
83+
"background_g": LaunchConfiguration("background_g"),
84+
"background_b": LaunchConfiguration("background_b"),
85+
}
86+
],
87+
),
88+
# perform remap so both turtles listen to the same command topic
89+
Node(
90+
package="turtlesim",
91+
executable="mimic",
92+
name="mimic",
93+
remappings=[
94+
("/input/pose", "/turtlesim1/turtle1/pose"),
95+
("/output/cmd_vel", "/turtlesim2/turtle1/cmd_vel"),
96+
],
97+
),
98+
]
99+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!-- from https://docs.ros.org/en/rolling/How-To-Guides/Launch-file-different-formats.html -->
3+
<launch>
4+
<!-- args that can be set from the command line or a default will be used -->
5+
<arg name="background_r" default="0" />
6+
<arg name="background_g" default="255" />
7+
<arg name="background_b" default="0" />
8+
<arg name="chatter_py_ns" default="chatter/py/ns" />
9+
<arg name="chatter_xml_ns" default="chatter/xml/ns" />
10+
<arg name="chatter_yaml_ns" default="chatter/yaml/ns" />
11+
12+
<!-- include another launch file -->
13+
<include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py" />
14+
<!--<include file="$(find-pkg-share demo_nodes_cpp_comment)/launch/topics/talker_listener_launch.py" />-->
15+
<!-- This is multiline comment
16+
this pkg should not be found!!
17+
<include file="$(find-pkg-share demo_nodes_cpp_comment)/launch/topics/talker_listener_launch.py" />
18+
-->
19+
<!-- include a Python launch file in the chatter_py_ns namespace-->
20+
<group>
21+
<!-- push_ros_namespace to set namespace of included nodes -->
22+
<push_ros_namespace namespace="$(var chatter_py_ns)" />
23+
<include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py" />
24+
</group>
25+
26+
<!-- include a xml launch file in the chatter_xml_ns namespace-->
27+
<group>
28+
<!-- push_ros_namespace to set namespace of included nodes -->
29+
<push_ros_namespace namespace="$(var chatter_xml_ns)" />
30+
<include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.xml" />
31+
</group>
32+
33+
<!-- include a yaml launch file in the chatter_yaml_ns namespace-->
34+
<group>
35+
<!-- push_ros_namespace to set namespace of included nodes -->
36+
<push_ros_namespace namespace="$(var chatter_yaml_ns)" />
37+
<include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.yaml" />
38+
</group>
39+
40+
<!-- start a turtlesim_node in the turtlesim1 namespace -->
41+
<node pkg="turtlesim" exec="turtlesim_node" name="sim" namespace="turtlesim1" />
42+
43+
<!-- start another turtlesim_node in the turtlesim2 namespace and use args to set parameters -->
44+
<node pkg="turtlesim" exec="turtlesim_node" name="sim" namespace="turtlesim2">
45+
<param name="background_r" value="$(var background_r)" />
46+
<param name="background_g" value="$(var background_g)" />
47+
<param name="background_b" value="$(var background_b)" />
48+
</node>
49+
50+
<!-- perform remap so both turtles listen to the same command topic -->
51+
<node pkg="turtlesim" exec="mimic" name="mimic">
52+
<remap from="/input/pose" to="/turtlesim1/turtle1/pose" />
53+
<remap from="/output/cmd_vel" to="/turtlesim2/turtle1/cmd_vel" />
54+
</node>
55+
</launch>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
%YAML 1.2
2+
# extracted from https://docs.ros.org/en/rolling/How-To-Guides/Launch-file-different-formats.html
3+
---
4+
launch:
5+
# args that can be set from the command line or a default will be used
6+
- arg:
7+
name: "background_r"
8+
default: "0"
9+
- arg:
10+
name: "background_g"
11+
default: "255"
12+
- arg:
13+
name: "background_b"
14+
default: "0"
15+
- arg:
16+
name: "chatter_py_ns"
17+
default: "chatter/py/ns"
18+
- arg:
19+
name: "chatter_xml_ns"
20+
default: "chatter/xml/ns"
21+
- arg:
22+
name: "chatter_yaml_ns"
23+
default: "chatter/yaml/ns"
24+
25+
# include another launch file
26+
- include:
27+
file: "$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py"
28+
29+
# include a Python launch file in the chatter_py_ns namespace
30+
- group:
31+
- push_ros_namespace:
32+
namespace: "$(var chatter_py_ns)"
33+
- include:
34+
file: "$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py"
35+
#- include:
36+
# file: "$(find-pkg-share demo_nodes_cpp_comment)/launch/topics/talker_listener_launch.py"
37+
38+
# include a xml launch file in the chatter_xml_ns namespace
39+
- group:
40+
- push_ros_namespace:
41+
namespace: "$(var chatter_xml_ns)"
42+
- include:
43+
file: "$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.xml"
44+
45+
# include a yaml launch file in the chatter_yaml_ns namespace
46+
- group:
47+
- push_ros_namespace:
48+
namespace: "$(var chatter_yaml_ns)"
49+
- include:
50+
file: "$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.yaml"
51+
52+
# start a turtlesim_node in the turtlesim1 namespace
53+
- node:
54+
pkg: "turtlesim"
55+
exec: "turtlesim_node"
56+
name: "sim"
57+
namespace: "turtlesim1"
58+
59+
# start another turtlesim_node in the turtlesim2 namespace and use args to set parameters
60+
- node:
61+
pkg: "turtlesim"
62+
exec: "turtlesim_node"
63+
name: "sim"
64+
namespace: "turtlesim2"
65+
param:
66+
- name: "background_r"
67+
value: "$(var background_r)"
68+
- name: "background_g"
69+
value: "$(var background_g)"
70+
- name: "background_b"
71+
value: "$(var background_b)"
72+
73+
# perform remap so both turtles listen to the same command topic
74+
- node:
75+
pkg: "turtlesim"
76+
exec: "mimic"
77+
name: "mimic"
78+
remap:
79+
- from: "/input/pose"
80+
to: "/turtlesim1/turtle1/pose"
81+
- from: "/output/cmd_vel"
82+
to: "/turtlesim2/turtle1/cmd_vel"

0 commit comments

Comments
 (0)