Skip to content

Commit 71b5053

Browse files
committed
refactor: Address position issues with unicode text
1 parent ebf5466 commit 71b5053

File tree

3 files changed

+97
-98
lines changed

3 files changed

+97
-98
lines changed

src/bors/handlers/trybuild.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::github::{
2020
CommitSha, GithubUser, LabelTrigger, MergeError, PullRequest, PullRequestNumber,
2121
};
2222
use crate::permissions::PermissionType;
23-
use crate::utils::suppress_github_mentions;
23+
use crate::utils::text::suppress_github_mentions;
2424

2525
use super::deny_request;
2626
use super::has_permission;

src/utils/mod.rs

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,3 @@
11
pub mod logging;
2+
pub mod text;
23
pub mod timing;
3-
4-
/// Converts GitHub @mentions to markdown-backticked text to prevent notifications.
5-
/// For example, "@user" becomes "`user`".
6-
///
7-
/// Handles GitHub mention formats:
8-
/// - Usernames (@username)
9-
/// - Teams (@org/team)
10-
/// - Nested teams (@org/team/subteam)
11-
///
12-
/// GitHub's nested team documentation:
13-
/// https://docs.github.com/en/organizations/organizing-members-into-teams/about-teams#nested-teams
14-
///
15-
/// Ignores email addresses and other @ symbols that don't match GitHub mention patterns.
16-
pub fn suppress_github_mentions(text: &str) -> String {
17-
if text.is_empty() || !text.contains('@') {
18-
return text.to_string();
19-
}
20-
21-
let segment = r"[A-Za-z0-9][A-Za-z0-9\-]{0,38}";
22-
let pattern = format!(r"@{0}(?:/{0})*", segment);
23-
24-
let re = regex::Regex::new(&pattern).unwrap();
25-
re.replace_all(text, |caps: &regex::Captures| {
26-
let mention = &caps[0];
27-
let position = caps.get(0).unwrap().start();
28-
29-
if !is_github_mention(text, mention, position) {
30-
return mention.to_string();
31-
}
32-
33-
let name = &mention[1..]; // Drop the @ symbol
34-
format!("`{}`", name)
35-
})
36-
.to_string()
37-
}
38-
39-
// Determines if a potential mention would actually trigger a notification
40-
fn is_github_mention(text: &str, mention: &str, pos: usize) -> bool {
41-
// Not a valid mention if preceded by alphanumeric or underscore (email)
42-
if pos > 0 {
43-
let c = text.chars().nth(pos - 1).unwrap();
44-
if c.is_alphanumeric() || c == '_' {
45-
return false;
46-
}
47-
}
48-
49-
// Check if followed by invalid character
50-
let end = pos + mention.len();
51-
if end < text.len() {
52-
let next_char = text.chars().nth(end).unwrap();
53-
if next_char.is_alphanumeric() || next_char == '_' || next_char == '-' {
54-
return false;
55-
}
56-
}
57-
58-
true
59-
}
60-
61-
#[cfg(test)]
62-
mod tests {
63-
use super::*;
64-
65-
#[test]
66-
fn test_suppress_github_mentions() {
67-
// User mentions
68-
assert_eq!(suppress_github_mentions("Hello @user"), "Hello `user`");
69-
70-
// Org team mentions
71-
assert_eq!(suppress_github_mentions("@org/team"), "`org/team`");
72-
assert_eq!(
73-
suppress_github_mentions("@org/team/subteam"),
74-
"`org/team/subteam`"
75-
);
76-
assert_eq!(
77-
suppress_github_mentions("@big/team/sub/group"),
78-
"`big/team/sub/group`"
79-
);
80-
assert_eq!(
81-
suppress_github_mentions("Thanks @user, @rust-lang/libs and @github/docs/content!"),
82-
"Thanks `user`, `rust-lang/libs` and `github/docs/content`!"
83-
);
84-
85-
// Non mentions
86-
assert_eq!(suppress_github_mentions("@"), "@");
87-
assert_eq!(suppress_github_mentions(""), "");
88-
assert_eq!(
89-
suppress_github_mentions("No mentions here"),
90-
"No mentions here"
91-
);
92-
assert_eq!(
93-
suppress_github_mentions("user@example.com"),
94-
"user@example.com"
95-
);
96-
97-
assert_eq!(suppress_github_mentions("@user_test"), "@user_test");
98-
}
99-
}

src/utils/text.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use regex::{Captures, Regex};
2+
3+
/// Replaces valid GitHub @mentions with backticks to prevent accidental pings
4+
///
5+
/// For example:
6+
/// "@user" -> "`@user`".
7+
/// "@org/team" -> "`@org/team`".
8+
/// "@org/team/subteam" -> "`@org/team/subteam`".
9+
pub fn suppress_github_mentions(text: &str) -> String {
10+
if text.is_empty() || !text.contains('@') {
11+
return text.to_string();
12+
}
13+
14+
let segment = r"[a-zA-Z0-9][a-zA-Z0-9\-]{0,38}";
15+
let pattern = format!(
16+
r"(^|[^a-zA-Z0-9_])(@{0}(?:/{0})*)($|[^a-zA-Z0-9_\-])",
17+
segment
18+
);
19+
20+
let re = Regex::new(&pattern).unwrap();
21+
re.replace_all(text, |caps: &Captures| {
22+
// Group 1: The character before the mention (or start of string)
23+
// Group 2: The @mention itself
24+
// Group 3: The character after the mention (or end of string)
25+
format!("{}`{}`{}", &caps[1], &caps[2], &caps[3])
26+
})
27+
.to_string()
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
34+
#[test]
35+
fn basic_mentions() {
36+
assert_eq!(suppress_github_mentions("Hello @user"), "Hello `@user`");
37+
assert_eq!(
38+
suppress_github_mentions("Ping @developer"),
39+
"Ping `@developer`"
40+
);
41+
assert_eq!(
42+
suppress_github_mentions("Multiple @user1 and @user2"),
43+
"Multiple `@user1` and `@user2`"
44+
);
45+
}
46+
47+
#[test]
48+
fn team_mentions() {
49+
assert_eq!(suppress_github_mentions("@org/team"), "`@org/team`");
50+
assert_eq!(
51+
suppress_github_mentions("@rust-lang/libs"),
52+
"`@rust-lang/libs`"
53+
);
54+
assert_eq!(
55+
suppress_github_mentions("@org/team/subteam"),
56+
"`@org/team/subteam`"
57+
);
58+
}
59+
60+
#[test]
61+
fn mention_boundaries() {
62+
// Adjacent punctuation
63+
assert_eq!(
64+
suppress_github_mentions("Hello,@user! How are you?"),
65+
"Hello,`@user`! How are you?"
66+
);
67+
68+
// Email addresses
69+
assert_eq!(
70+
suppress_github_mentions("user@example.com"),
71+
"user@example.com"
72+
);
73+
74+
// Invalid mentions
75+
assert_eq!(suppress_github_mentions("@-user"), "@-user");
76+
assert_eq!(suppress_github_mentions("word@user"), "word@user");
77+
assert_eq!(suppress_github_mentions("@user_next"), "@user_next");
78+
}
79+
80+
#[test]
81+
fn edge_cases() {
82+
// Empty input
83+
assert_eq!(suppress_github_mentions(""), "");
84+
85+
// Minimum valid mention
86+
assert_eq!(suppress_github_mentions("@a"), "`@a`");
87+
88+
// Maximum length mention
89+
let long_mention = "@".to_string() + &"a".repeat(39);
90+
assert_eq!(
91+
suppress_github_mentions(&long_mention),
92+
format!("`{long_mention}`")
93+
);
94+
}
95+
}

0 commit comments

Comments
 (0)