File: BuildCheck\Checks\EmbeddedResourceCheck.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO;
using System.Collections.Generic;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Experimental.BuildCheck.Checks;
internal class EmbeddedResourceCheck : Check
{
    private const string RuleId = "BC0105";
    public static CheckRule SupportedRule = new CheckRule(RuleId, "EmbeddedResourceCulture",
        ResourceUtilities.GetResourceString("BuildCheck_BC0105_Title")!,
        ResourceUtilities.GetResourceString("BuildCheck_BC0105_MessageFmt")!,
        new CheckConfiguration() { RuleId = RuleId, Severity = CheckResultSeverity.Warning });
 
    public override string FriendlyName => "MSBuild.EmbeddedResourceCulture";
 
    public override IReadOnlyList<CheckRule> SupportedRules { get; } = [SupportedRule];
 
    public override void Initialize(ConfigurationContext configurationContext)
    {
        /* This is it - no custom configuration */
    }
 
    public override void RegisterActions(IBuildCheckRegistrationContext registrationContext)
    {
        registrationContext.RegisterEvaluatedItemsAction(EvaluatedItemsAction);
    }
 
    internal override bool IsBuiltIn => true;
 
    private readonly HashSet<string> _projects = new(MSBuildNameIgnoreCaseComparer.Default);
 
    private void EvaluatedItemsAction(BuildCheckDataContext<EvaluatedItemsCheckData> context)
    {
        // Deduplication
        if (!_projects.Add(context.Data.ProjectFilePath))
        {
            return;
        }
 
        foreach (ItemData itemData in context.Data.EnumerateItemsOfType(ItemNames.EmbeddedResource))
        {
            string evaluatedEmbedItem = itemData.EvaluatedInclude;
            bool hasDoubleExtension = HasDoubleExtension(evaluatedEmbedItem);
 
            if (!hasDoubleExtension)
            {
                continue;
            }
 
            bool hasNeededMetadata = false;
            foreach (KeyValuePair<string, string> keyValuePair in itemData.EnumerateMetadata())
            {
                if (MSBuildNameIgnoreCaseComparer.Default.Equals(keyValuePair.Key, ItemMetadataNames.culture))
                {
                    hasNeededMetadata = true;
                    break;
                }
 
                if (MSBuildNameIgnoreCaseComparer.Default.Equals(keyValuePair.Key, ItemMetadataNames.withCulture) &&
                    keyValuePair.Value.IsMSBuildFalseString())
                {
                    hasNeededMetadata = true;
                    break;
                }
            }
 
            if (!hasNeededMetadata)
            {
                context.ReportResult(BuildCheckResult.Create(
                    SupportedRule,
                    // Populating precise location tracked via https://github.com/dotnet/msbuild/issues/10383
                    ElementLocation.EmptyLocation,
                    Path.GetFileName(context.Data.ProjectFilePath),
                    evaluatedEmbedItem,
                    GetSupposedCultureExtension(evaluatedEmbedItem)));
            }
        }
    }
 
    private static bool HasDoubleExtension(string s)
    {
        const char extensionSeparator = '.';
        int firstIndex;
        return
            !string.IsNullOrEmpty(s) &&
            (firstIndex = s.IndexOf(extensionSeparator)) > -1 &&
            // We need at least 2 chars for this extension - separator and one char of extension,
            // so next extension can start closest 2 chars from this one
            // (this is to grace handle double dot - which is not double extension)
            firstIndex + 2 <= s.Length &&
            s.IndexOf(extensionSeparator, firstIndex + 2) > -1;
    }
 
    /// <summary>
    /// Returns the extension that is supposed to implicitly denote the culture.
    /// This is mimicking the behavior of Microsoft.Build.Tasks.Culture.GetItemCultureInfo
    /// </summary>
    private string GetSupposedCultureExtension(string s)
    {
        // If the item is defined as "Strings.en-US.resx", then we want to arrive to 'en-US'
 
        string extension = Path.GetExtension(Path.GetFileNameWithoutExtension(s));
        if (extension.Length > 1)
        {
            extension = extension.Substring(1);
        }
        return extension;
    }
}