* implement a fix for tags being found in code blocks still needs polish, consider this a rough draft * refactor to reuse a local function uses CheckWrappedInCode to check that there are no code blocks that surround the tag being parsed * Add more test coverage of MessageHelper.ParseTags * reset indexes for @ here mention * add a test case to catch error fixed from prev commit * wip commit of most test cases working * fix the Enclosed in block util method * code cleanup * lint whitespace * lint brackets for single line if blocks * move messagehelpertests to the new unit test dir * expose internals to the unit test project this seems to have been breaking the build, since CI would build the merged branch, where rest wasn't exposed to the unit teststags/2.2.0
| @@ -1,3 +1,4 @@ | |||||
| using System.Runtime.CompilerServices; | |||||
| using System.Runtime.CompilerServices; | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] | |||||
| @@ -6,4 +6,5 @@ using System.Runtime.CompilerServices; | |||||
| [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Webhook")] | [assembly: InternalsVisibleTo("Discord.Net.Webhook")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Commands")] | [assembly: InternalsVisibleTo("Discord.Net.Commands")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] | [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] | ||||
| @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Webhook")] | [assembly: InternalsVisibleTo("Discord.Net.Webhook")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Commands")] | [assembly: InternalsVisibleTo("Discord.Net.Commands")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | [assembly: InternalsVisibleTo("Discord.Net.Tests")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] | |||||
| [assembly: TypeForwardedTo(typeof(Discord.Embed))] | [assembly: TypeForwardedTo(typeof(Discord.Embed))] | ||||
| [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] | [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] | ||||
| @@ -3,6 +3,7 @@ using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Text.RegularExpressions; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Message; | using Model = Discord.API.Message; | ||||
| @@ -108,14 +109,56 @@ namespace Discord.Rest | |||||
| public static ImmutableArray<ITag> ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection<IUser> userMentions) | public static ImmutableArray<ITag> ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection<IUser> userMentions) | ||||
| { | { | ||||
| var tags = ImmutableArray.CreateBuilder<ITag>(); | var tags = ImmutableArray.CreateBuilder<ITag>(); | ||||
| int index = 0; | int index = 0; | ||||
| var codeIndex = 0; | |||||
| var inlineRegex = new Regex(@"[^\\]?(`).+?[^\\](`)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); | |||||
| var blockRegex = new Regex(@"[^\\]?(```).+?[^\\](```)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); | |||||
| // checks if the tag being parsed is wrapped in code blocks | |||||
| bool CheckWrappedCode() | |||||
| { | |||||
| // util to check if the index of a tag is within the bounds of the codeblock | |||||
| bool EnclosedInBlock(Match m) | |||||
| => m.Groups[1].Index < index && index < m.Groups[2].Index; | |||||
| // loop through all code blocks that are before the start of the tag | |||||
| while (codeIndex < index) | |||||
| { | |||||
| var blockMatch = blockRegex.Match(text, codeIndex); | |||||
| if (blockMatch.Success) | |||||
| { | |||||
| if (EnclosedInBlock(blockMatch)) | |||||
| return true; | |||||
| // continue if the end of the current code was before the start of the tag | |||||
| codeIndex += blockMatch.Groups[2].Index + blockMatch.Groups[2].Length; | |||||
| if (codeIndex < index) | |||||
| continue; | |||||
| return false; | |||||
| } | |||||
| var inlineMatch = inlineRegex.Match(text, codeIndex); | |||||
| if (inlineMatch.Success) | |||||
| { | |||||
| if (EnclosedInBlock(inlineMatch)) | |||||
| return true; | |||||
| // continue if the end of the current code was before the start of the tag | |||||
| codeIndex += inlineMatch.Groups[2].Index + inlineMatch.Groups[2].Length; | |||||
| if (codeIndex < index) | |||||
| continue; | |||||
| return false; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| while (true) | while (true) | ||||
| { | { | ||||
| index = text.IndexOf('<', index); | index = text.IndexOf('<', index); | ||||
| if (index == -1) break; | if (index == -1) break; | ||||
| int endIndex = text.IndexOf('>', index + 1); | int endIndex = text.IndexOf('>', index + 1); | ||||
| if (endIndex == -1) break; | if (endIndex == -1) break; | ||||
| if (CheckWrappedCode()) break; | |||||
| string content = text.Substring(index, endIndex - index + 1); | string content = text.Substring(index, endIndex - index + 1); | ||||
| if (MentionUtils.TryParseUser(content, out ulong id)) | if (MentionUtils.TryParseUser(content, out ulong id)) | ||||
| @@ -158,10 +201,12 @@ namespace Discord.Rest | |||||
| } | } | ||||
| index = 0; | index = 0; | ||||
| codeIndex = 0; | |||||
| while (true) | while (true) | ||||
| { | { | ||||
| index = text.IndexOf("@everyone", index); | index = text.IndexOf("@everyone", index); | ||||
| if (index == -1) break; | if (index == -1) break; | ||||
| if (CheckWrappedCode()) break; | |||||
| var tagIndex = FindIndex(tags, index); | var tagIndex = FindIndex(tags, index); | ||||
| if (tagIndex.HasValue) | if (tagIndex.HasValue) | ||||
| tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.EveryoneMention, index, "@everyone".Length, 0, guild?.EveryoneRole)); | tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.EveryoneMention, index, "@everyone".Length, 0, guild?.EveryoneRole)); | ||||
| @@ -169,10 +214,12 @@ namespace Discord.Rest | |||||
| } | } | ||||
| index = 0; | index = 0; | ||||
| codeIndex = 0; | |||||
| while (true) | while (true) | ||||
| { | { | ||||
| index = text.IndexOf("@here", index); | index = text.IndexOf("@here", index); | ||||
| if (index == -1) break; | if (index == -1) break; | ||||
| if (CheckWrappedCode()) break; | |||||
| var tagIndex = FindIndex(tags, index); | var tagIndex = FindIndex(tags, index); | ||||
| if (tagIndex.HasValue) | if (tagIndex.HasValue) | ||||
| tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.HereMention, index, "@here".Length, 0, guild?.EveryoneRole)); | tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.HereMention, index, "@here".Length, 0, guild?.EveryoneRole)); | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Runtime.CompilerServices; | |||||
| using System.Runtime.CompilerServices; | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Relay")] | [assembly: InternalsVisibleTo("Discord.Net.Relay")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] | |||||
| @@ -1,3 +1,4 @@ | |||||
| using System.Runtime.CompilerServices; | |||||
| using System.Runtime.CompilerServices; | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] | |||||
| @@ -0,0 +1,118 @@ | |||||
| using Xunit; | |||||
| using Discord.Rest; | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> | |||||
| /// Tests for <see cref="MessageHelper"/> parsing. | |||||
| /// </summary> | |||||
| public class MessageHelperTests | |||||
| { | |||||
| /// <summary> | |||||
| /// Tests that no tags are parsed while in code blocks | |||||
| /// or inline code. | |||||
| /// </summary> | |||||
| [Theory] | |||||
| [InlineData("`@everyone`")] | |||||
| [InlineData("`<@163184946742034432>`")] | |||||
| [InlineData("```@everyone```")] | |||||
| [InlineData("```cs \n @everyone```")] | |||||
| [InlineData("```cs <@163184946742034432> ```")] | |||||
| [InlineData("``` test ``` ```cs <@163184946742034432> ```")] | |||||
| [InlineData("`<:test:537920404019216384>`")] | |||||
| [InlineData("``` @everyone `")] // discord client handles these weirdly | |||||
| [InlineData("``` @everyone ``")] | |||||
| [InlineData("` @here `")] | |||||
| [InlineData("` @everyone @here <@163184946742034432> <@&163184946742034432> <#163184946742034432> <:test:537920404019216384> `")] | |||||
| public void ParseTagsInCode(string testData) | |||||
| { | |||||
| // don't care that I'm passing in null channels/guilds/users | |||||
| // as they shouldn't be required | |||||
| var result = MessageHelper.ParseTags(testData, null, null, null); | |||||
| Assert.Empty(result); | |||||
| } | |||||
| /// <summary> Tests parsing tags that surround inline code or a code block. </summary> | |||||
| [Theory] | |||||
| [InlineData("`` <@&163184946742034432>")] | |||||
| [InlineData("``` code block 1 ``` ``` code block 2 ``` <@&163184946742034432>")] | |||||
| [InlineData("` code block 1 ``` ` code block 2 ``` <@&163184946742034432>")] | |||||
| [InlineData("<@&163184946742034432> ``` code block 1 ```")] | |||||
| [InlineData("``` code ``` ``` code ``` @here ``` code ``` ``` more ```")] | |||||
| [InlineData("``` code ``` @here ``` more ```")] | |||||
| public void ParseTagsAroundCode(string testData) | |||||
| { | |||||
| // don't care that I'm passing in null channels/guilds/users | |||||
| // as they shouldn't be required | |||||
| var result = MessageHelper.ParseTags(testData, null, null, null); | |||||
| Assert.NotEmpty(result); | |||||
| } | |||||
| [Theory] | |||||
| [InlineData(@"\` @everyone \`")] | |||||
| [InlineData(@"\`\`\` @everyone \`\`\`")] | |||||
| [InlineData(@"hey\`\`\`@everyone\`\`\`!!")] | |||||
| public void IgnoreEscapedCodeBlocks(string testData) | |||||
| { | |||||
| var result = MessageHelper.ParseTags(testData, null, null, null); | |||||
| Assert.NotEmpty(result); | |||||
| } | |||||
| // cannot test parsing a user, as it uses the ReadOnlyCollection<IUser> arg. | |||||
| // this could be done if mocked entities are merged in PR #1290 | |||||
| /// <summary> Tests parsing a mention of a role. </summary> | |||||
| [Theory] | |||||
| [InlineData("<@&163184946742034432>")] | |||||
| [InlineData("**<@&163184946742034432>**")] | |||||
| [InlineData("__<@&163184946742034432>__")] | |||||
| [InlineData("<><@&163184946742034432>")] | |||||
| public void ParseRole(string roleTag) | |||||
| { | |||||
| var result = MessageHelper.ParseTags(roleTag, null, null, null); | |||||
| Assert.Contains(result, x => x.Type == TagType.RoleMention); | |||||
| } | |||||
| /// <summary> Tests parsing a channel. </summary> | |||||
| [Theory] | |||||
| [InlineData("<#429115823748284417>")] | |||||
| [InlineData("**<#429115823748284417>**")] | |||||
| [InlineData("<><#429115823748284417>")] | |||||
| public void ParseChannel(string channelTag) | |||||
| { | |||||
| var result = MessageHelper.ParseTags(channelTag, null, null, null); | |||||
| Assert.Contains(result, x => x.Type == TagType.ChannelMention); | |||||
| } | |||||
| /// <summary> Tests parsing an emoji. </summary> | |||||
| [Theory] | |||||
| [InlineData("<:test:537920404019216384>")] | |||||
| [InlineData("**<:test:537920404019216384>**")] | |||||
| [InlineData("<><:test:537920404019216384>")] | |||||
| public void ParseEmoji(string emoji) | |||||
| { | |||||
| var result = MessageHelper.ParseTags(emoji, null, null, null); | |||||
| Assert.Contains(result, x => x.Type == TagType.Emoji); | |||||
| } | |||||
| /// <summary> Tests parsing a mention of @everyone. </summary> | |||||
| [Theory] | |||||
| [InlineData("@everyone")] | |||||
| [InlineData("**@everyone**")] | |||||
| public void ParseEveryone(string everyone) | |||||
| { | |||||
| var result = MessageHelper.ParseTags(everyone, null, null, null); | |||||
| Assert.Contains(result, x => x.Type == TagType.EveryoneMention); | |||||
| } | |||||
| /// <summary> Tests parsing a mention of @here. </summary> | |||||
| [Theory] | |||||
| [InlineData("@here")] | |||||
| [InlineData("**@here**")] | |||||
| public void ParseHere(string here) | |||||
| { | |||||
| var result = MessageHelper.ParseTags(here, null, null, null); | |||||
| Assert.Contains(result, x => x.Type == TagType.HereMention); | |||||
| } | |||||
| } | |||||
| } | |||||