@@ -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
0 commit comments