File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\Core\Utilities\NameGenerator.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Shared.Utilities;
 
internal static class NameGenerator
{
    /// <summary>
    /// Transforms baseName into a name that does not conflict with any name in 'reservedNames'
    /// </summary>
    public static string EnsureUniqueness(
        string baseName,
        IEnumerable<string> reservedNames,
        bool isCaseSensitive = true)
    {
        using var nameSetPool = (isCaseSensitive ? SharedPools.StringHashSet : SharedPools.StringIgnoreCaseHashSet).GetPooledObject();
        var nameSet = nameSetPool.Object;
 
        nameSet.AddRange(reservedNames);
 
        var index = 1;
        var result = baseName;
 
        while (nameSet.Contains(result))
        {
            result = baseName + index;
            index++;
        }
 
        return result;
    }
 
    public static ImmutableArray<string> EnsureUniqueness(
        ImmutableArray<string> names,
        Func<string, bool>? canUse = null,
        bool isCaseSensitive = true)
    {
        using var isFixedDisposer = ArrayBuilder<bool>.GetInstance(names.Length, fillWithValue: false, out var isFixed);
 
        var result = ArrayBuilder<string>.GetInstance(names.Length);
        result.AddRange(names);
        EnsureUniquenessInPlace(result, isFixed, canUse, isCaseSensitive);
        return result.ToImmutableAndFree();
    }
 
    /// <summary>
    /// Ensures that any 'names' is unique and does not collide with any other name.  Names that
    /// are marked as IsFixed can not be touched.  This does mean that if there are two names
    /// that are the same, and both are fixed that you will end up with non-unique names at the
    /// end.
    /// </summary>
    public static ImmutableArray<string> EnsureUniqueness(
        ImmutableArray<string> names,
        ImmutableArray<bool> isFixed,
        Func<string, bool>? canUse = null,
        bool isCaseSensitive = true)
    {
        using var _1 = ArrayBuilder<bool>.GetInstance(names.Length, out var isFixedBuilder);
        using var _2 = ArrayBuilder<string>.GetInstance(names.Length, out var result);
 
        isFixedBuilder.AddRange(isFixed);
        result.AddRange(names);
 
        EnsureUniquenessInPlace(result, isFixedBuilder, canUse, isCaseSensitive);
 
        return result.ToImmutableAndClear();
    }
 
    /// <summary>
    /// Updates the names in <paramref name="names"/> to be unique.  A name at a particular
    /// index <c>i</c> will not be touched if <c>isFixed[i]</c> is <see langword="true"/>. All
    /// other names will not collide with any other in <paramref name="names"/> and will all
    /// return <see langword="true"/> for <c>canUse(name)</c>.
    /// </summary>
    public static void EnsureUniquenessInPlace(
        ArrayBuilder<string> names,
        ArrayBuilder<bool> isFixed,
        Func<string, bool>? canUse = null,
        bool isCaseSensitive = true)
    {
        canUse ??= Functions<string>.True;
 
        using var usedNamesPool = (isCaseSensitive ? SharedPools.StringHashSet : SharedPools.StringIgnoreCaseHashSet).GetPooledObject();
        var usedNames = usedNamesPool.Object;
 
        using var collisionMapPool = (isCaseSensitive ? SharedPools.Default<Dictionary<string, bool>>() : SharedPools.StringIgnoreCaseDictionary<bool>()).GetPooledObject();
        var collisionMap = collisionMapPool.Object;
 
        // Initial pass through names to determine which names are in collision
        foreach (var name in names)
        {
            var isCollision = collisionMap.ContainsKey(name);
 
            collisionMap[name] = isCollision;
 
            if (isCollision)
                usedNames.Remove(name);
            else
                usedNames.Add(name);
        }
 
        // Update any name that is in collision (and not fixed) to have
        //   a new name that hasn't yet been placed in usedNames.
        for (var i = 0; i < names.Count; i++)
        {
            if (isFixed[i])
                continue;
 
            var name = names[i];
            if (!collisionMap[name] && canUse(name))
                continue;
 
            var index = 1;
            var updatedName = name + index;
            while (usedNames.Contains(updatedName) || !canUse(updatedName))
            {
                index++;
                updatedName = name + index;
            }
 
            usedNames.Add(updatedName);
            names[i] = updatedName;
        }
    }
 
    public static string GenerateUniqueName(string baseName, Func<string, bool> canUse)
        => GenerateUniqueName(baseName, string.Empty, canUse);
 
    public static string GenerateUniqueName(string baseName, ISet<string> names, StringComparer comparer)
        => GenerateUniqueName(baseName, x => !names.Contains(x, comparer));
 
    public static string GenerateUniqueName(string baseName, string extension, Func<string, bool> canUse)
    {
        if (!string.IsNullOrEmpty(extension) && extension[0] != '.')
        {
            extension = "." + extension;
        }
 
        var name = baseName + extension;
        var index = 1;
 
        // Check for collisions
        while (!canUse(name))
        {
            name = baseName + index + extension;
            index++;
        }
 
        return name;
    }
 
    public static string GenerateUniqueName(IEnumerable<string> baseNames, Func<string, bool> canUse)
    {
        int? index = null;
 
        while (true)
        {
            foreach (var name in baseNames)
            {
                var modifiedName = name + index;
 
                if (canUse(modifiedName))
                    return modifiedName;
            }
 
            index = index is null ? 1 : index + 1;
        }
    }
}