Skip to content
Open
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
46 changes: 44 additions & 2 deletions src/Cli/func/Actions/LocalActions/CreateFunctionAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,12 @@ public override async Task RunAsync()
ConfigureAuthorizationLevel(template);
}

ColoredConsole.Write($"Function name: [{template.Metadata.DefaultFunctionName}] ");
var defaultFunctionName = GetUniqueDefaultFunctionName(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this PR only covers the traditional/Node v4 path, .NET (in-proc and isolated) and the Python v2 programming model take their own branches earlier in RunAsync and still silently prompt with a colliding default. Please extend the uniqueness behavior to those two paths as well.

  • .NET (in-proc & isolated) branch: lines 144–162 (if (WorkerRuntimeLanguageHelper.IsDotnet(_workerRuntime) && !Csx)), default name is set at line 158 (FunctionName = string.IsNullOrEmpty(FunctionName) ? TemplateName : FunctionName;).

  • Python v2 (new programming model) branch: lines 165–234 (else if (IsNewPythonProgrammingModel())), default name is resolved at lines 227–230 (if (string.IsNullOrEmpty(FunctionName)) FunctionName = providedInputs[GetFunctionNameParamId];).

Both bypass the new GetUniqueDefaultFunctionName call

template.Metadata.DefaultFunctionName,
functionName => FunctionArtifactExists(functionName, FileName, template));
Comment on lines +273 to +275
Copy link
Copy Markdown
Member

@liliankasem liliankasem May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When FileName is set, FunctionArtifactExists ignores the candidate name and only checks the fixed file path. If that file already exists, every candidate returns true and GetUniqueDefaultFunctionName loops forever, hanging func new before it ever prompts. Easiest fix is to skip the uniqueness loop when FileName != null (renaming the function can't avoid a fixed-file collision anyway).

Cleanest fix:

var defaultFunctionName = FileName != null
    ? template.Metadata.DefaultFunctionName
    : GetUniqueDefaultFunctionName(
        template.Metadata.DefaultFunctionName,
        functionName => FunctionArtifactExists(functionName, FileName, template));

ColoredConsole.Write($"Function name: [{defaultFunctionName}] ");
FunctionName = FunctionName ?? Console.ReadLine();
FunctionName = string.IsNullOrEmpty(FunctionName) ? template.Metadata.DefaultFunctionName : FunctionName;
FunctionName = string.IsNullOrEmpty(FunctionName) ? defaultFunctionName : FunctionName;
await _templatesManager.Deploy(FunctionName, FileName, template);
PerformPostDeployTasks(FunctionName, Language);
}
Expand Down Expand Up @@ -470,6 +473,45 @@ private void ConfigureAuthorizationLevel(Template template)
}
}

internal static string GetUniqueDefaultFunctionName(string defaultFunctionName, Func<string, bool> functionExists)
{
if (!functionExists(defaultFunctionName))
{
return defaultFunctionName;
}

var index = 1;
string candidate;
do
{
candidate = $"{defaultFunctionName}{index}";
index++;
}
while (functionExists(candidate));

return candidate;
}

private static bool FunctionArtifactExists(string functionName, string fileName, Template template)
{
if (IsNodeV4Template(template))
{
return template.Files
.Where(file => !file.Key.EndsWith(".dat"))
.Select(file => fileName ?? file.Key.Replace("%functionName%", functionName))
.Any(name => FileSystemHelpers.FileExists(
Path.Combine(Environment.CurrentDirectory, "src", "functions", name)));
}

return FileSystemHelpers.DirectoryExists(Path.Combine(Environment.CurrentDirectory, functionName));
}

private static bool IsNodeV4Template(Template template)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exact code/check already exists inlined in TemplatesManager.cs:145 - you should extract this a shared helper instead of duplicating

{
return template.Id.EndsWith("JavaScript-4.x", StringComparison.OrdinalIgnoreCase) ||
template.Id.EndsWith("TypeScript-4.x", StringComparison.OrdinalIgnoreCase);
}

private bool InferAndUpdateLanguage(WorkerRuntime workerRuntime)
{
switch (workerRuntime)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using System.Collections.Generic;
using Azure.Functions.Cli.Actions.LocalActions;
using Xunit;

namespace Azure.Functions.Cli.UnitTests.ActionsTests
{
public class CreateFunctionActionTests
{
[Fact]
public void GetUniqueDefaultFunctionName_ReturnsDefault_WhenAvailable()
{
var result = CreateFunctionAction.GetUniqueDefaultFunctionName("HttpTrigger", _ => false);

Assert.Equal("HttpTrigger", result);
}

[Fact]
public void GetUniqueDefaultFunctionName_IncrementsUntilNameIsAvailable()
{
var existingNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"HttpTrigger",
"HttpTrigger1",
"HttpTrigger2"
};

var result = CreateFunctionAction.GetUniqueDefaultFunctionName("HttpTrigger", existingNames.Contains);

Assert.Equal("HttpTrigger3", result);
}
}
}
Loading