Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions src/Tasks.UnitTests/ResourceHandling/GenerateResource_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,227 @@ public void STRWithResourcesNamespaceAndSTRNamespaceVB()
{
Utilities.STRNamespaceTestHelper("VB", "MyResourcesNamespace", "MySTClassNamespace", _output);
}

/// <summary>
/// When a resource key matches a reserved name,
/// the STR property is skipped and a warning MSB3827 is logged.
/// The task should still succeed.
/// </summary>
[Fact]
public void StronglyTypedResources_ReservedName_LogsWarning()
{
GenerateResource t = Utilities.CreateTask(_output);
try
{
// Create a resx with a "ResourceManager" and "Culture" keys, which collides with the
// reserved property name added by StronglyTypedResourceBuilder.
string resxFile = Utilities.WriteTestResX(false, null,
" <data name=\"ResourceManager\">\r\n" +
" <value>ShouldBeSkipped</value>\r\n" +
" </data>\r\n" +
" <data name=\"Culture\">\r\n" +
" <value>ShouldBeSkipped</value>\r\n" +
" </data>\r\n");

t.Sources = new ITaskItem[] { new TaskItem(resxFile) };
t.StronglyTypedLanguage = "CSharp";
t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache"));

Utilities.ExecuteTask(t);

// The file should still be generated — only the reserved-name properties are skipped.
File.Exists(t.StronglyTypedFileName).ShouldBeTrue();
string generatedCode = File.ReadAllText(t.StronglyTypedFileName);

// "MyString" (the standard test resource) should still appear.
generatedCode.ShouldContain("MyString");

// The skipped resources should NOT have a GetString accessor in the generated code.
generatedCode.ShouldNotContain("GetString(\"ResourceManager\"");
generatedCode.ShouldNotContain("GetString(\"Culture\"");

// Warning about the reserved name should be logged.
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedReservedName", "ResourceManager", "ResourceManager, Culture");
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedReservedName", "Culture", "ResourceManager, Culture");

// Exactly 2 warnings (one per reserved name), no errors.
((MockEngine)t.BuildEngine).Warnings.ShouldBe(2);
((MockEngine)t.BuildEngine).Errors.ShouldBe(0);
}
finally
{
File.Delete(t.Sources[0].ItemSpec);
if (t.StronglyTypedFileName != null)
{
FileUtilities.DeleteNoThrow(t.StronglyTypedFileName);
}

foreach (ITaskItem item in t.FilesWritten)
{
FileUtilities.DeleteNoThrow(item.ItemSpec);
}
Comment thread
OvesN marked this conversation as resolved.
}
}

/// <summary>
/// When a resource key cannot be converted to a valid identifier (e.g. "=", "`"),
/// the STR property is skipped and a warning MSB3828 is logged.
/// The "=" characters are not in the s_charsToReplace list, so they survive
/// character replacement, and cannot become a valid C# identifier even after
/// CreateValidIdentifier and underscore-prepend attempts.
/// </summary>
[Fact]
public void StronglyTypedResources_InvalidIdentifier_LogsWarning()
{
GenerateResource t = Utilities.CreateTask(_output);
try
{
string resxFile = Utilities.WriteTestResX(false, null,
" <data name=\"=\">\r\n" +
" <value>EqualSign</value>\r\n" +
" </data>\r\n" +
" <data name=\"`\">\r\n" +
" <value>Backtick</value>\r\n" +
" </data>\r\n");

t.Sources = new ITaskItem[] { new TaskItem(resxFile) };
t.StronglyTypedLanguage = "CSharp";
t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache"));

Utilities.ExecuteTask(t);

File.Exists(t.StronglyTypedFileName).ShouldBeTrue();
string generatedCode = File.ReadAllText(t.StronglyTypedFileName);
generatedCode.ShouldContain("MyString");

// The skipped resource should NOT have a GetString accessor in the generated code.
generatedCode.ShouldNotContain("GetString(\"=\"");
generatedCode.ShouldNotContain("GetString(\"`\"");

// Warning about invalid identifier should be logged.
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedInvalidIdentifier", "=");
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedInvalidIdentifier", "`");

// Exactly 2 warnings, no errors.
((MockEngine)t.BuildEngine).Warnings.ShouldBe(2);
((MockEngine)t.BuildEngine).Errors.ShouldBe(0);
}
finally
{
File.Delete(t.Sources[0].ItemSpec);
if (t.StronglyTypedFileName != null)
{
FileUtilities.DeleteNoThrow(t.StronglyTypedFileName);
}

foreach (ITaskItem item in t.FilesWritten)
{
FileUtilities.DeleteNoThrow(item.ItemSpec);
}
}
}

/// <summary>
/// When two resource keys collide after identifier normalization (e.g. "foo-bar" and "foo_bar"),
/// both are skipped and a warning MSB3829 is logged for each, mentioning the other colliding key.
/// </summary>
[Fact]
public void StronglyTypedResources_NameCollision_LogsWarning()
{
GenerateResource t = Utilities.CreateTask(_output);
try
{
// "foo-bar" normalizes to "foo_bar", which collides with "foo_bar".
string resxFile = Utilities.WriteTestResX(false, null,
" <data name=\"foo-bar\">\r\n" +
" <value>Value1</value>\r\n" +
" </data>\r\n" +
" <data name=\"foo_bar\">\r\n" +
" <value>Value2</value>\r\n" +
" </data>\r\n");

t.Sources = new ITaskItem[] { new TaskItem(resxFile) };
t.StronglyTypedLanguage = "CSharp";
t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache"));

Utilities.ExecuteTask(t);

File.Exists(t.StronglyTypedFileName).ShouldBeTrue();
string generatedCode = File.ReadAllText(t.StronglyTypedFileName);

// "MyString" should still be generated.
generatedCode.ShouldContain("MyString");

// Neither colliding key should produce a property.
generatedCode.ShouldNotContain("foo_bar");

// Both colliding keys should have a warning referencing the other.
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedNameCollision", "foo-bar", "foo_bar");
Utilities.AssertLogContainsResource(t, "GenerateResource.STRPropertySkippedNameCollision", "foo_bar", "foo-bar");

// Exactly 2 warnings (one per colliding key), no errors.
((MockEngine)t.BuildEngine).Warnings.ShouldBe(2);
((MockEngine)t.BuildEngine).Errors.ShouldBe(0);
}
finally
{
File.Delete(t.Sources[0].ItemSpec);
if (t.StronglyTypedFileName != null)
{
FileUtilities.DeleteNoThrow(t.StronglyTypedFileName);
}

foreach (ITaskItem item in t.FilesWritten)
{
FileUtilities.DeleteNoThrow(item.ItemSpec);
}
}
}

/// <summary>
/// When all resources are valid, no skip warnings are logged —
/// verifies that warnings are only emitted when needed.
/// </summary>
[Fact]
public void StronglyTypedResources_NoProblematicKeys_NoSkipWarnings()
{
GenerateResource t = Utilities.CreateTask(_output);
try
{
string textFile = Utilities.WriteTestText(null, null);
t.Sources = new ITaskItem[] { new TaskItem(textFile) };
t.StronglyTypedLanguage = "CSharp";
t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache"));

Utilities.ExecuteTask(t);

File.Exists(t.StronglyTypedFileName).ShouldBeTrue();

// None of the skip warnings should appear.
string log = ((MockEngine)t.BuildEngine).Log;
log.ShouldNotContain("MSB3827");
log.ShouldNotContain("MSB3828");
log.ShouldNotContain("MSB3829");
log.ShouldNotContain("MSB3830");

// No warnings or errors at all.
((MockEngine)t.BuildEngine).Warnings.ShouldBe(0);
((MockEngine)t.BuildEngine).Errors.ShouldBe(0);
}
finally
{
File.Delete(t.Sources[0].ItemSpec);
if (t.StronglyTypedFileName != null)
{
FileUtilities.DeleteNoThrow(t.StronglyTypedFileName);
}

foreach (ITaskItem item in t.FilesWritten)
{
FileUtilities.DeleteNoThrow(item.ItemSpec);
}
}
}
}

public sealed class TransformationErrors
Expand Down
29 changes: 16 additions & 13 deletions src/Tasks/GenerateResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3443,7 +3443,7 @@ private void CreateStronglyTypedResources(ReaderInfo reader, String outFile, Str
_logger.LogMessageFromResources("GenerateResource.CreatingSTR", _stronglyTypedFilename);

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

CodeGeneratorOptions codeGenOptions = new CodeGeneratorOptions();
using (TextWriter output = new StreamWriter(_stronglyTypedFilename))
{
provider.GenerateCodeFromCompileUnit(ccu, output, codeGenOptions);
}

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

_stronglyTypedResourceSuccessfullyCreated = true;
}
finally
{
Expand Down
17 changes: 17 additions & 0 deletions src/Tasks/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,23 @@
<comment>{StrBegin="MSB3826: "}</comment>
</data>

<data name="GenerateResource.STRPropertySkippedReservedName">
<value>MSB3827: Skipping strongly typed resource property generation for resource "{0}": the name conflicts with a reserved name. Reserved names: {1}.</value>
<comment>{StrBegin="MSB3827: "}</comment>
</data>
<data name="GenerateResource.STRPropertySkippedInvalidIdentifier">
<value>MSB3828: Skipping strongly typed resource property generation for resource "{0}": the name cannot be converted to a valid identifier.</value>
<comment>{StrBegin="MSB3828: "}</comment>
</data>
<data name="GenerateResource.STRPropertySkippedNameCollision">
<value>MSB3829: Skipping strongly typed resource property generation for resource "{0}": a name collision exists with resource "{1}" after identifier normalization.</value>
<comment>{StrBegin="MSB3829: "}</comment>
</data>
<data name="GenerateResource.STRPropertySkippedVoidType">
<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>
Comment thread
OvesN marked this conversation as resolved.
<comment>{StrBegin="MSB3830: "}</comment>
</data>

<!--
The GetAssemblyIdentity message bucket is: MSB3441 - MSB3450

Expand Down
20 changes: 20 additions & 0 deletions src/Tasks/Resources/xlf/Strings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions src/Tasks/Resources/xlf/Strings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading