diff --git a/Content.Tests/DMProject/Tests/Preprocessor/macro_path_double_slash.dm b/Content.Tests/DMProject/Tests/Preprocessor/macro_path_double_slash.dm new file mode 100644 index 0000000000..6f7976f2a6 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Preprocessor/macro_path_double_slash.dm @@ -0,0 +1,47 @@ +//# issue 2649 + +// A macro that expands to a path can end up pasted directly after a slash, producing an +// empty path element. BYOND ignores those. This can only happen through macro expansion, +// since a literal "//" in source is a comment. + +#define LEADING /datum/first +#define MIDDLE /second +#define TRAILING /first + +// "//datum/first" +/LEADING + var/x = 1 + +// "/datum/first//second" +/datum/first/MIDDLE + var/y = 2 + +// The shape goonstation's namespace macros expand to, which relies on "##" pasting a +// leading slash onto a trailing one. +#define IDENTITY(_ARGS...) ##_ARGS +#define NS_PATH(_NAME) /datum/namespace/##_NAME +#define NS_PATH_I(_NAME) /datum/namespace/##_NAME/##IDENTITY +#define CREATE_NS(a, b) NS_PATH_I(a)(var/##NS_PATH_I(a##__##b)(##b = NS_PATH(a##__##b))) +#define ADD_TO_NS(a, b) NS_PATH_I(a##__##b) + +/datum/namespace +/datum/namespace/RADIO +/datum/namespace/RADIO__COL + +CREATE_NS(RADIO, COL) +ADD_TO_NS(RADIO, COL)(var/const/BRIG = "#FF5000") + +/proc/RunTest() + var/datum/first/F = new + ASSERT(F.x == 1) + + var/datum/first/second/S = new + ASSERT(S.x == 1) + ASSERT(S.y == 2) + + // "/datum//first" in an expression + ASSERT(/datum/TRAILING == /datum/first) + + var/datum/namespace/RADIO/N = new + ASSERT(N.COL == /datum/namespace/RADIO__COL) + ASSERT(/datum/namespace/RADIO__COL::BRIG == "#FF5000") diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index aefd37901c..d6a3b0b2b3 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -398,12 +398,12 @@ public DMASTFile File() { if (expression) return null; } - string? pathElement = PathElement(); + string? pathElement = PathElementSkippingEmpty(); if (pathElement != null) { List pathElements = [pathElement]; bool operatorFlag = false; while (pathElement != null && Check(TokenType.DM_Slash)) { - pathElement = PathElement(); + pathElement = PathElementSkippingEmpty(); if (pathElement != null) { if(pathElement == "operator") { @@ -449,6 +449,32 @@ public DMASTFile File() { return null; } + /// + /// Reads the next path element, ignoring any empty ones (the "//" in "/datum//foo"). + /// BYOND accepts these; they can only come from macro expansion because "//" is a comment in source. + /// + /// The next path element, or null if there isn't one. Slashes read by a failed attempt are given back. + private string? PathElementSkippingEmpty() { + string? pathElement = PathElement(); + if (pathElement != null || Current().Type != TokenType.DM_Slash) + return pathElement; + + List skippedSlashes = new(); + do { + skippedSlashes.Add(Current()); + Advance(); + + pathElement = PathElement(); + if (pathElement != null) + return pathElement; + } while (Current().Type == TokenType.DM_Slash); + + for (int i = skippedSlashes.Count - 1; i >= 0; i--) + ReuseToken(skippedSlashes[i]); + + return null; + } + /// /// Extracts the text from this token if it is reasonable for it to appear as a typename in a path. ///