Skip to content

Commit ab7dbb6

Browse files
OvesNCopilot
andauthored
Log warnings for skipped STR resource keys instead of failing the build (dotnet#13291)
Fixes dotnet#13061 ### Context The `GenerateResource` task with `MSBuild:Compile` failed the entire build when a resource key (e.g. containing = or \`) could not be converted to a valid C# identifier. In contrast, `ResXFileCodeGenerator` silently skipped such entries. This aligns the behavior by skipping invalid keys with warnings instead of failing. ### Changes Made - Added warning messages MSB3827–MSB3830 for resource keys that are skipped during STR property generation. - STR files are now still generated even when some keys cannot produce valid identifiers. ### Testing Added unit tests verifying that invalid resource keys emit the expected warnings and valid keys still generate STR properties. ### Notes `ResXFileCodeGenerator `in VS silently skips invalid keys with no diagnostic. A separate VS [issue ](https://developercommunity.visualstudio.com/t/ResXFileCodeGenerator-silently-skips-res/11049047?) has been created to add warnings there as well. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent b8f5ea0 commit ab7dbb6

17 files changed

+581
-38
lines changed

src/Tasks.UnitTests/ResourceHandling/GenerateResource_Tests.cs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,6 +1665,227 @@ public void STRWithResourcesNamespaceAndSTRNamespaceVB()
16651665
{
16661666
Utilities.STRNamespaceTestHelper("VB", "MyResourcesNamespace", "MySTClassNamespace", _output);
16671667
}
1668+
1669+
/// <summary>
1670+
/// When a resource key matches a reserved name,
1671+
/// the STR property is skipped and a warning MSB3827 is logged.
1672+
/// The task should still succeed.
1673+
/// </summary>
1674+
[Fact]
1675+
public void StronglyTypedResources_ReservedName_LogsWarning()
1676+
{
1677+
GenerateResource t = Utilities.CreateTask(_output);
1678+
try
1679+
{
1680+
// Create a resx with a "ResourceManager" and "Culture" keys, which collides with the
1681+
// reserved property name added by StronglyTypedResourceBuilder.
1682+
string resxFile = Utilities.WriteTestResX(false, null,
1683+
" <data name=\"ResourceManager\">\r\n" +
1684+
" <value>ShouldBeSkipped</value>\r\n" +
1685+
" </data>\r\n" +
1686+
" <data name=\"Culture\">\r\n" +
1687+
" <value>ShouldBeSkipped</value>\r\n" +
1688+
" </data>\r\n");
1689+
1690+
t.Sources = new ITaskItem[] { new TaskItem(resxFile) };
1691+
t.StronglyTypedLanguage = "CSharp";
1692+
t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache"));
1693+
1694+
Utilities.ExecuteTask(t);
1695+
1696+
// The file should still be generated — only the reserved-name properties are skipped.
1697+
File.Exists(t.StronglyTypedFileName).ShouldBeTrue();
1698+
string generatedCode = File.ReadAllText(t.StronglyTypedFileName);
1699+
1700+
// "MyString" (the standard test resource) should still appear.
1701+
generatedCode.ShouldContain("MyString");
1702+
1703+
// The skipped resources should NOT have a GetString accessor in the generated code.
1704+
generatedCode.ShouldNotContain("GetString(\"ResourceManager\"");
1705+
generatedCode.ShouldNotContain("GetString(\"Culture\"");
1706+
1707+
// Warning about the reserved name should be logged.
1708+
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedReservedName", "ResourceManager", "ResourceManager, Culture");
1709+
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedReservedName", "Culture", "ResourceManager, Culture");
1710+
1711+
// Exactly 2 warnings (one per reserved name), no errors.
1712+
((MockEngine)t.BuildEngine).Warnings.ShouldBe(2);
1713+
((MockEngine)t.BuildEngine).Errors.ShouldBe(0);
1714+
}
1715+
finally
1716+
{
1717+
File.Delete(t.Sources[0].ItemSpec);
1718+
if (t.StronglyTypedFileName != null)
1719+
{
1720+
FileUtilities.DeleteNoThrow(t.StronglyTypedFileName);
1721+
}
1722+
1723+
foreach (ITaskItem item in t.FilesWritten)
1724+
{
1725+
FileUtilities.DeleteNoThrow(item.ItemSpec);
1726+
}
1727+
}
1728+
}
1729+
1730+
/// <summary>
1731+
/// When a resource key cannot be converted to a valid identifier (e.g. "=", "`"),
1732+
/// the STR property is skipped and a warning MSB3828 is logged.
1733+
/// The "=" characters are not in the s_charsToReplace list, so they survive
1734+
/// character replacement, and cannot become a valid C# identifier even after
1735+
/// CreateValidIdentifier and underscore-prepend attempts.
1736+
/// </summary>
1737+
[Fact]
1738+
public void StronglyTypedResources_InvalidIdentifier_LogsWarning()
1739+
{
1740+
GenerateResource t = Utilities.CreateTask(_output);
1741+
try
1742+
{
1743+
string resxFile = Utilities.WriteTestResX(false, null,
1744+
" <data name=\"=\">\r\n" +
1745+
" <value>EqualSign</value>\r\n" +
1746+
" </data>\r\n" +
1747+
" <data name=\"`\">\r\n" +
1748+
" <value>Backtick</value>\r\n" +
1749+
" </data>\r\n");
1750+
1751+
t.Sources = new ITaskItem[] { new TaskItem(resxFile) };
1752+
t.StronglyTypedLanguage = "CSharp";
1753+
t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache"));
1754+
1755+
Utilities.ExecuteTask(t);
1756+
1757+
File.Exists(t.StronglyTypedFileName).ShouldBeTrue();
1758+
string generatedCode = File.ReadAllText(t.StronglyTypedFileName);
1759+
generatedCode.ShouldContain("MyString");
1760+
1761+
// The skipped resource should NOT have a GetString accessor in the generated code.
1762+
generatedCode.ShouldNotContain("GetString(\"=\"");
1763+
generatedCode.ShouldNotContain("GetString(\"`\"");
1764+
1765+
// Warning about invalid identifier should be logged.
1766+
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedInvalidIdentifier", "=");
1767+
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedInvalidIdentifier", "`");
1768+
1769+
// Exactly 2 warnings, no errors.
1770+
((MockEngine)t.BuildEngine).Warnings.ShouldBe(2);
1771+
((MockEngine)t.BuildEngine).Errors.ShouldBe(0);
1772+
}
1773+
finally
1774+
{
1775+
File.Delete(t.Sources[0].ItemSpec);
1776+
if (t.StronglyTypedFileName != null)
1777+
{
1778+
FileUtilities.DeleteNoThrow(t.StronglyTypedFileName);
1779+
}
1780+
1781+
foreach (ITaskItem item in t.FilesWritten)
1782+
{
1783+
FileUtilities.DeleteNoThrow(item.ItemSpec);
1784+
}
1785+
}
1786+
}
1787+
1788+
/// <summary>
1789+
/// When two resource keys collide after identifier normalization (e.g. "foo-bar" and "foo_bar"),
1790+
/// both are skipped and a warning MSB3829 is logged for each, mentioning the other colliding key.
1791+
/// </summary>
1792+
[Fact]
1793+
public void StronglyTypedResources_NameCollision_LogsWarning()
1794+
{
1795+
GenerateResource t = Utilities.CreateTask(_output);
1796+
try
1797+
{
1798+
// "foo-bar" normalizes to "foo_bar", which collides with "foo_bar".
1799+
string resxFile = Utilities.WriteTestResX(false, null,
1800+
" <data name=\"foo-bar\">\r\n" +
1801+
" <value>Value1</value>\r\n" +
1802+
" </data>\r\n" +
1803+
" <data name=\"foo_bar\">\r\n" +
1804+
" <value>Value2</value>\r\n" +
1805+
" </data>\r\n");
1806+
1807+
t.Sources = new ITaskItem[] { new TaskItem(resxFile) };
1808+
t.StronglyTypedLanguage = "CSharp";
1809+
t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache"));
1810+
1811+
Utilities.ExecuteTask(t);
1812+
1813+
File.Exists(t.StronglyTypedFileName).ShouldBeTrue();
1814+
string generatedCode = File.ReadAllText(t.StronglyTypedFileName);
1815+
1816+
// "MyString" should still be generated.
1817+
generatedCode.ShouldContain("MyString");
1818+
1819+
// Neither colliding key should produce a property.
1820+
generatedCode.ShouldNotContain("foo_bar");
1821+
1822+
// Both colliding keys should have a warning referencing the other.
1823+
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedNameCollision", "foo-bar", "foo_bar");
1824+
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedNameCollision", "foo_bar", "foo-bar");
1825+
1826+
// Exactly 2 warnings (one per colliding key), no errors.
1827+
((MockEngine)t.BuildEngine).Warnings.ShouldBe(2);
1828+
((MockEngine)t.BuildEngine).Errors.ShouldBe(0);
1829+
}
1830+
finally
1831+
{
1832+
File.Delete(t.Sources[0].ItemSpec);
1833+
if (t.StronglyTypedFileName != null)
1834+
{
1835+
FileUtilities.DeleteNoThrow(t.StronglyTypedFileName);
1836+
}
1837+
1838+
foreach (ITaskItem item in t.FilesWritten)
1839+
{
1840+
FileUtilities.DeleteNoThrow(item.ItemSpec);
1841+
}
1842+
}
1843+
}
1844+
1845+
/// <summary>
1846+
/// When all resources are valid, no skip warnings are logged —
1847+
/// verifies that warnings are only emitted when needed.
1848+
/// </summary>
1849+
[Fact]
1850+
public void StronglyTypedResources_NoProblematicKeys_NoSkipWarnings()
1851+
{
1852+
GenerateResource t = Utilities.CreateTask(_output);
1853+
try
1854+
{
1855+
string textFile = Utilities.WriteTestText(null, null);
1856+
t.Sources = new ITaskItem[] { new TaskItem(textFile) };
1857+
t.StronglyTypedLanguage = "CSharp";
1858+
t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache"));
1859+
1860+
Utilities.ExecuteTask(t);
1861+
1862+
File.Exists(t.StronglyTypedFileName).ShouldBeTrue();
1863+
1864+
// None of the skip warnings should appear.
1865+
string log = ((MockEngine)t.BuildEngine).Log;
1866+
log.ShouldNotContain("MSB3827");
1867+
log.ShouldNotContain("MSB3828");
1868+
log.ShouldNotContain("MSB3829");
1869+
log.ShouldNotContain("MSB3830");
1870+
1871+
// No warnings or errors at all.
1872+
((MockEngine)t.BuildEngine).Warnings.ShouldBe(0);
1873+
((MockEngine)t.BuildEngine).Errors.ShouldBe(0);
1874+
}
1875+
finally
1876+
{
1877+
File.Delete(t.Sources[0].ItemSpec);
1878+
if (t.StronglyTypedFileName != null)
1879+
{
1880+
FileUtilities.DeleteNoThrow(t.StronglyTypedFileName);
1881+
}
1882+
1883+
foreach (ITaskItem item in t.FilesWritten)
1884+
{
1885+
FileUtilities.DeleteNoThrow(item.ItemSpec);
1886+
}
1887+
}
1888+
}
16681889
}
16691890

16701891
public sealed class TransformationErrors

src/Tasks/GenerateResource.cs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3443,7 +3443,7 @@ private void CreateStronglyTypedResources(ReaderInfo reader, String outFile, Str
34433443
_logger.LogMessageFromResources("GenerateResource.CreatingSTR", _stronglyTypedFilename);
34443444

34453445
// Generate the STR class
3446-
String[] errors;
3446+
StronglyTypedResourceBuilder.SkippedResource[] skippedResources;
34473447
bool generateInternalClass = !_stronglyTypedClassIsPublic;
34483448
// StronglyTypedResourcesNamespace can be null and this is ok.
34493449
// If it is null then the default namespace (=stronglyTypedNamespace) is used.
@@ -3454,28 +3454,31 @@ private void CreateStronglyTypedResources(ReaderInfo reader, String outFile, Str
34543454
_stronglyTypedResourcesNamespace,
34553455
provider,
34563456
generateInternalClass,
3457-
out errors);
3457+
out skippedResources);
34583458

34593459
CodeGeneratorOptions codeGenOptions = new CodeGeneratorOptions();
34603460
using (TextWriter output = new StreamWriter(_stronglyTypedFilename))
34613461
{
34623462
provider.GenerateCodeFromCompileUnit(ccu, output, codeGenOptions);
34633463
}
34643464

3465-
if (errors.Length > 0)
3465+
// The STR class file was generated (possibly with some properties skipped).
3466+
// Log warnings for any skipped resources and mark the file as successfully created.
3467+
foreach (StronglyTypedResourceBuilder.SkippedResource skipped in skippedResources)
34663468
{
3467-
_logger.LogErrorWithCodeFromResources("GenerateResource.ErrorFromCodeDom", inputFileName);
3468-
foreach (String error in errors)
3469+
string messageKey = skipped.Reason switch
34693470
{
3470-
_logger.LogErrorWithCodeFromResources("GenerateResource.CodeDomError", error);
3471-
}
3472-
}
3473-
else
3474-
{
3475-
// No errors, and no exceptions - we presumably did create the STR class file
3476-
// and it should get added to FilesWritten. So set a flag to indicate this.
3477-
_stronglyTypedResourceSuccessfullyCreated = true;
3471+
StronglyTypedResourceBuilder.SkipReason.CodeDomError => "GenerateResource.CodeDomError",
3472+
StronglyTypedResourceBuilder.SkipReason.ReservedName => "GenerateResource.STRPropertySkippedReservedName",
3473+
StronglyTypedResourceBuilder.SkipReason.VoidType => "GenerateResource.STRPropertySkippedVoidType",
3474+
StronglyTypedResourceBuilder.SkipReason.InvalidIdentifier => "GenerateResource.STRPropertySkippedInvalidIdentifier",
3475+
StronglyTypedResourceBuilder.SkipReason.NameCollision => "GenerateResource.STRPropertySkippedNameCollision",
3476+
_ => throw new InvalidOperationException($"Unknown SkipReason {skipped.Reason}"),
3477+
};
3478+
_logger.LogWarningWithCodeFromResources(null, Path.GetFullPath(inputFileName), 0, 0, 0, 0, messageKey, [skipped.Key, .. skipped.AdditionalMessageArgs]);
34783479
}
3480+
3481+
_stronglyTypedResourceSuccessfullyCreated = true;
34793482
}
34803483
finally
34813484
{

src/Tasks/Resources/Strings.resx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,23 @@
11791179
<comment>{StrBegin="MSB3826: "}</comment>
11801180
</data>
11811181

1182+
<data name="GenerateResource.STRPropertySkippedReservedName">
1183+
<value>MSB3827: Skipping strongly typed resource property generation for resource "{0}": the name conflicts with a reserved name. Reserved names: {1}.</value>
1184+
<comment>{StrBegin="MSB3827: "}</comment>
1185+
</data>
1186+
<data name="GenerateResource.STRPropertySkippedInvalidIdentifier">
1187+
<value>MSB3828: Skipping strongly typed resource property generation for resource "{0}": the name cannot be converted to a valid identifier.</value>
1188+
<comment>{StrBegin="MSB3828: "}</comment>
1189+
</data>
1190+
<data name="GenerateResource.STRPropertySkippedNameCollision">
1191+
<value>MSB3829: Skipping strongly typed resource property generation for resource "{0}": a name collision exists with resource "{1}" after identifier normalization.</value>
1192+
<comment>{StrBegin="MSB3829: "}</comment>
1193+
</data>
1194+
<data name="GenerateResource.STRPropertySkippedVoidType">
1195+
<value>MSB3830: Skipping strongly typed resource property generation for resource "{0}": the resource value type is void, which cannot be used as a property type.</value>
1196+
<comment>{StrBegin="MSB3830: "}</comment>
1197+
</data>
1198+
11821199
<!--
11831200
The GetAssemblyIdentity message bucket is: MSB3441 - MSB3450
11841201

src/Tasks/Resources/xlf/Strings.cs.xlf

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Tasks/Resources/xlf/Strings.de.xlf

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)