From 93f4546242f410f0e68686f2c22b3722a9e3d71d Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sun, 12 Jul 2026 22:09:28 -0700 Subject: [PATCH] Concatenate adjacent string literals in deprecated attribute messages A C deprecated message can be written as adjacent string literals, which the compiler concatenates into a single value. The generator extracted the text between the first and last quote verbatim, so the emitted Obsolete attribute contained the raw boundary quotes and inter-literal whitespace, producing invalid C#. Scan the attribute source for each literal and join their contents into a single valid Obsolete message. Fixes #694 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.cs | 51 ++++++++++++++++--- .../DeprecatedAdjacentStringTest.cs | 40 +++++++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/DeprecatedAdjacentStringTest.cs diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs index aba0735e..56191fd8 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs @@ -2060,14 +2060,11 @@ private void WithAttributes(NamedDecl namedDecl, bool onlySupportedOSPlatform = } var attrText = GetSourceRangeContents(namedDecl.TranslationUnit.Handle, attr.Extent); + var message = GetDeprecatedMessage(attrText); - var textStart = attrText.IndexOf('"', StringComparison.Ordinal); - var textLength = attrText.LastIndexOf('"') - textStart; - - if (textLength > 1) + if (!string.IsNullOrEmpty(message)) { - var text = attrText.AsSpan(textStart + 1, textLength - 1); - outputBuilder.WriteCustomAttribute($"Obsolete(\"{text}\")"); + outputBuilder.WriteCustomAttribute($"Obsolete(\"{message}\")"); } else { @@ -2109,6 +2106,48 @@ private void WithAttributes(NamedDecl namedDecl, bool onlySupportedOSPlatform = } } + private static string? GetDeprecatedMessage(string attrText) + { + // The attribute source looks like `deprecated("message")`, but C allows the message to be + // written as adjacent string literals (e.g. `deprecated("part1" "part2")`), which the + // compiler concatenates into a single value. Gather the contents of every literal and join + // them so the emitted `Obsolete("...")` is a single, valid C# string. + + var builder = new StringBuilder(); + var hasLiteral = false; + var index = 0; + + while (index < attrText.Length) + { + if (attrText[index] != '"') + { + index++; + continue; + } + + hasLiteral = true; + index++; + + while ((index < attrText.Length) && (attrText[index] != '"')) + { + // Preserve escape sequences verbatim so an escaped quote isn't treated as the end + // of the literal and the contents are emitted unchanged (as before). + if ((attrText[index] == '\\') && ((index + 1) < attrText.Length)) + { + _ = builder.Append(attrText[index]); + index++; + } + + _ = builder.Append(attrText[index]); + index++; + } + + index++; + } + + return hasLiteral ? builder.ToString() : null; + } + private string GetLibraryPath(string remappedName) { return !_config.WithLibraryPaths.TryGetValue(remappedName, out var libraryPath) && !_config.WithLibraryPaths.TryGetValue("*", out libraryPath) diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/DeprecatedAdjacentStringTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/DeprecatedAdjacentStringTest.cs new file mode 100644 index 00000000..3b659878 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/DeprecatedAdjacentStringTest.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. + +using System.Threading.Tasks; +using NUnit.Framework; + +namespace ClangSharp.UnitTests; + +/// +/// Regression test for https://github.com/dotnet/ClangSharp/issues/694. +/// A deprecated message written as adjacent C string literals must be concatenated into a single +/// C# string literal, otherwise the emitted [Obsolete(...)] attribute is invalid C#. +/// +[Platform("win")] +public sealed class DeprecatedAdjacentStringTest : PInvokeGeneratorTest +{ + [Test] + public Task AdjacentStringLiteralsAreConcatenated() + { + var inputContents = @"extern ""C"" [[deprecated(""Use Bar() or "" + ""Baz() instead"")]] +void MyFunction(); +"; + + var expectedOutputContents = @"using System; +using System.Runtime.InteropServices; + +namespace ClangSharp.Test +{ + public static partial class Methods + { + [DllImport(""ClangSharpPInvokeGenerator"", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [Obsolete(""Use Bar() or Baz() instead"")] + public static extern void MyFunction(); + } +} +"; + + return ValidateGeneratedCSharpLatestWindowsBindingsAsync(inputContents, expectedOutputContents); + } +}