Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Cyclic Dependencies in Go #5467

Merged
merged 13 commits into from
Oct 7, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Fixed cyclic depencies in generated Go code. [#2834](https://github.com/microsoft/kiota/issues/2834)

## [1.19.0] - 2024-10-03

### Added
Expand Down
12 changes: 0 additions & 12 deletions it/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,6 @@
}
],
"Suppressions": [
{
"Language": "go",
"Rationale": "https://github.com/microsoft/kiota/issues/3436"
},
{
"Language": "ruby",
"Rationale": "https://github.com/microsoft/kiota/issues/2484"
Expand Down Expand Up @@ -237,10 +233,6 @@
"Language": "typescript",
"Rationale": "https://github.com/microsoft/kiota/issues/5256"
},
{
"Language": "go",
"Rationale": "https://github.com/microsoft/kiota/issues/2834"
},
{
"Language": "java",
"Rationale": "https://github.com/microsoft/kiota/issues/2842"
Expand All @@ -259,10 +251,6 @@
}
],
"IdempotencySuppressions": [
{
"Language": "go",
"Rationale": "https://github.com/microsoft/kiota/issues/2834"
},
{
"Language": "java",
"Rationale": "https://github.com/microsoft/kiota/issues/2842"
Expand Down
4 changes: 2 additions & 2 deletions src/Kiota.Builder/CodeDOM/CodeNamespace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ public CodeNamespace GetRootNamespace()
public IEnumerable<CodeInterface> Interfaces => InnerChildElements.Values.OfType<CodeInterface>();
public IEnumerable<CodeConstant> Constants => InnerChildElements.Values.OfType<CodeConstant>();
public IEnumerable<CodeFile> Files => InnerChildElements.Values.OfType<CodeFile>();
public CodeNamespace? FindNamespaceByName(string nsName)
public CodeNamespace? FindNamespaceByName(string nsName, bool findInChildElements = false)
{
ArgumentException.ThrowIfNullOrEmpty(nsName);
if (nsName.Equals(Name, StringComparison.OrdinalIgnoreCase)) return this;
var result = FindChildByName<CodeNamespace>(nsName, false);
var result = FindChildByName<CodeNamespace>(nsName, findInChildElements);
if (result == null)
foreach (var childNS in InnerChildElements.Values.OfType<CodeNamespace>())
{
Expand Down
214 changes: 205 additions & 9 deletions src/Kiota.Builder/Refiners/GoRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
static x => x.ToFirstCharacterLowerCase(),
GenerationLanguage.Go);
FlattenNestedHierarchy(generatedCode);
FlattenGoParamsFileNames(generatedCode);
FlattenGoFileNames(generatedCode);
FlattenParamsFileNames(generatedCode);
FlattenFileNames(generatedCode);
NormalizeNamespaceNames(generatedCode);
AddInnerClasses(
generatedCode,
Expand All @@ -46,7 +46,7 @@
string.Empty,
false,
MergeOverLappedStrings);
if (_configuration.ExcludeBackwardCompatible) //TODO remove condition for v2

Check warning on line 49 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)

Check warning on line 49 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
RemoveRequestConfigurationClasses(generatedCode,
new CodeUsing
{
Expand Down Expand Up @@ -190,12 +190,13 @@
);
AddParsableImplementsForModelClasses(
generatedCode,
"Parsable"

Check warning on line 193 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'Parsable' 5 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)

Check warning on line 193 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'Parsable' 5 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
);
RenameInnerModelsToAppended(
generatedCode
);
cancellationToken.ThrowIfCancellationRequested();
CorrectCyclicReference(generatedCode);
CopyModelClassesAsInterfaces(
generatedCode,
x => $"{x.Name}able"
Expand All @@ -218,6 +219,203 @@
}, cancellationToken);
}

private void CorrectCyclicReference(CodeElement currentElement)
{
var currentNameSpace = currentElement.GetImmediateParentOfType<CodeNamespace>();
var modelsNameSpace = findClientNameSpace(currentNameSpace)
?.FindNamespaceByName(
$"{_configuration.ClientNamespaceName}.{GenerationConfiguration.ModelsNamespaceSegmentName}");

if (modelsNameSpace == null)
return;

var dependencies = new Dictionary<string, HashSet<string>>();
GetUsingsInModelsNameSpace(modelsNameSpace, modelsNameSpace, dependencies);

var migratedNamespaces = new Dictionary<string, string>();
var cycles = FindCycles(dependencies);
foreach (var cycle in cycles)
{
foreach (var cycleReference in cycle.Value)
{
var dupNs = cycleReference[^2]; // 2nd last element is target base namespace
var nameSpace = modelsNameSpace.FindNamespaceByName(dupNs, true);

migratedNamespaces[dupNs] = modelsNameSpace.Name;
MigrateNameSpace(nameSpace!, modelsNameSpace);

if (!cycle.Key.Equals(modelsNameSpace.Name, StringComparison.OrdinalIgnoreCase)
&& !migratedNamespaces.ContainsKey(cycle.Key)
&& modelsNameSpace.FindNamespaceByName(cycle.Key, true) is { } currentNs)
{
migratedNamespaces[cycle.Key] = modelsNameSpace.Name;
MigrateNameSpace(currentNs, modelsNameSpace);
}
}
}

CorrectReferencesToMigratedModels(currentElement, migratedNamespaces);
}

private string GetComposedName(CodeElement codeClass)
{
var classNameList = getPathsName(codeClass, codeClass.Name);
return string.Join(string.Empty, classNameList.Count > 1 ? classNameList.Skip(1) : classNameList);
}

private void GetUsingsInModelsNameSpace(CodeNamespace modelsNameSpace, CodeNamespace currentNameSpace, Dictionary<string, HashSet<string>> dependencies)
rkodev marked this conversation as resolved.
Show resolved Hide resolved
{
if (!modelsNameSpace.Name.Equals(currentNameSpace.Name, StringComparison.OrdinalIgnoreCase) && !currentNameSpace.IsChildOf(modelsNameSpace))
return;

dependencies[currentNameSpace.Name] = currentNameSpace.Classes
.SelectMany(static codeClass => codeClass.Usings)
.Where(static x => x.Declaration != null && !x.Declaration.IsExternal)
.Select(static x => x.Declaration?.TypeDefinition?.GetImmediateParentOfType<CodeNamespace>())
.OfType<CodeNamespace>()
.Where(ns => !ns.Name.Equals(currentNameSpace.Name, StringComparison.OrdinalIgnoreCase))
.Select(static ns => ns.Name)
.ToHashSet();
rkodev marked this conversation as resolved.
Show resolved Hide resolved

foreach (var codeNameSpace in currentNameSpace.Namespaces.Where(codeNameSpace => !dependencies.ContainsKey(codeNameSpace.Name)))
{
GetUsingsInModelsNameSpace(modelsNameSpace, codeNameSpace, dependencies);
}
}

/// <summary>
/// Returns a dictionary of cycles in the graph with the key being the base namespace and the values being the path to the cycle
/// In GoLang, any self referencing namespace in a tree is a cycle, therefore the whole namespace is moved to the root
/// </summary>
/// <param name="dependencies"></param>
/// <returns></returns>
private static Dictionary<string, List<List<string>>> FindCycles(Dictionary<string, HashSet<string>> dependencies)
{
var cycles = new Dictionary<string, List<List<string>>>();
var visited = new HashSet<string>();
var stack = new Stack<string>();

foreach (var node in dependencies.Keys)
{
if (!visited.Contains(node))
baywet marked this conversation as resolved.
Show resolved Hide resolved
{
SearchCycles(node, node, dependencies, visited, stack, cycles);
}
}

return cycles;
}


/// <summary>
/// Performs a DFS search to find cycles in the graph. Method will stop at the first cycle found in each node
/// </summary>
private static void SearchCycles(string parentNode, string node, Dictionary<string, HashSet<string>> dependencies, HashSet<string> visited, Stack<string> stack, Dictionary<string, List<List<string>>> cycles)
{
visited.Add(node);
stack.Push(node);

if (dependencies.TryGetValue(node, out var value))
{
var stackSet = new HashSet<string>(stack);
foreach (var neighbor in value)
{
if (stackSet.Contains(neighbor))
{
var cycle = stack.Reverse().Concat([neighbor]).ToList();
if (!cycles.ContainsKey(parentNode))
cycles[parentNode] = new List<List<string>>();

cycles[parentNode].Add(cycle);
}
else if (!visited.Contains(neighbor))
{
SearchCycles(parentNode, neighbor, dependencies, visited, stack, cycles);
}
}
}

stack.Pop();
}

private void MigrateNameSpace(CodeNamespace currentNameSpace, CodeNamespace targetNameSpace)
{
foreach (var codeClass in currentNameSpace.Classes)
{
currentNameSpace.RemoveChildElement(codeClass);
codeClass.Name = GetComposedName(codeClass);
codeClass.Parent = targetNameSpace;
targetNameSpace.AddClass(codeClass);
}

foreach (var x in currentNameSpace.Enums)
{
currentNameSpace.RemoveChildElement(x);
x.Name = GetComposedName(x);
x.Parent = targetNameSpace;
targetNameSpace.AddEnum(x);
}

foreach (var x in currentNameSpace.Interfaces)
{
currentNameSpace.RemoveChildElement(x);
x.Name = GetComposedName(x);
x.Parent = targetNameSpace;
targetNameSpace.AddInterface(x);
}

foreach (var x in currentNameSpace.Functions)
{
currentNameSpace.RemoveChildElement(x);
x.Name = GetComposedName(x);
x.Parent = targetNameSpace;
targetNameSpace.AddFunction(x);
}

foreach (var x in currentNameSpace.Constants)
{
currentNameSpace.RemoveChildElement(x);
x.Name = GetComposedName(x);
x.Parent = targetNameSpace;
targetNameSpace.AddConstant(x);
}

foreach (var ns in currentNameSpace.Namespaces)
{
MigrateNameSpace(ns, targetNameSpace);
}
}
private void CorrectReferencesToMigratedModels(CodeElement currentElement, Dictionary<string, string> migratedNamespaces)
rkodev marked this conversation as resolved.
Show resolved Hide resolved
{
if (currentElement is CodeNamespace cn)
{
var usings = cn.GetChildElements()
.SelectMany(static x => x.GetChildElements())
.OfType<ProprietableBlockDeclaration>()
.SelectMany(static x => x.Usings)
.Where(x => migratedNamespaces.ContainsKey(x.Name))
.ToArray();

foreach (var codeUsing in usings)
{
if (codeUsing.Parent is not ProprietableBlockDeclaration blockDeclaration ||
codeUsing.Declaration?.TypeDefinition?.GetImmediateParentOfType<CodeNamespace>().Name.Equals(migratedNamespaces[codeUsing.Name], StringComparison.OrdinalIgnoreCase) == false
)
{
continue;
}

blockDeclaration.RemoveUsings(codeUsing);
blockDeclaration.AddUsings(new CodeUsing
{
Name = migratedNamespaces[codeUsing.Name],
Declaration = codeUsing.Declaration
});
}
}

CrawlTree(currentElement, x => CorrectReferencesToMigratedModels(x, migratedNamespaces));
}
private void GenerateCodeFiles(CodeElement currentElement)
{
if (currentElement is CodeInterface codeInterface && currentElement.Parent is CodeNamespace codeNamespace)
Expand All @@ -234,7 +432,7 @@
CrawlTree(currentElement, GenerateCodeFiles);
}

private string MergeOverLappedStrings(string start, string end)

Check warning on line 435 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'MergeOverLappedStrings' a static method. (https://rules.sonarsource.com/csharp/RSPEC-2325)
{
var search = "RequestBuilder";
start = start.ToFirstCharacterUpperCase();
Expand All @@ -247,7 +445,7 @@
return $"{start}{end}";
}

private void CorrectBackingStoreTypes(CodeElement currentElement)

Check warning on line 448 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (!_configuration.UsesBackingStore)
return;
Expand Down Expand Up @@ -343,11 +541,9 @@
var packageRootNameSpace = findNameSpaceAtLevel(parentNameSpace, currentNamespace, 1);
if (!packageRootNameSpace.Name.Equals(currentNamespace.Name, StringComparison.Ordinal) && modelNameSpace != null && !currentNamespace.IsChildOf(modelNameSpace))
{
var classNameList = getPathsName(codeClass, codeClass.Name);
var newClassName = string.Join(string.Empty, classNameList.Count > 1 ? classNameList.Skip(1) : classNameList);

currentNamespace.RemoveChildElement(codeClass);
codeClass.Name = newClassName;
codeClass.Name = GetComposedName(codeClass);
codeClass.Parent = packageRootNameSpace;
packageRootNameSpace.AddClass(codeClass);
}
Expand All @@ -356,7 +552,7 @@
CrawlTree(currentElement, FlattenNestedHierarchy);
}

private void FlattenGoParamsFileNames(CodeElement currentElement)
private void FlattenParamsFileNames(CodeElement currentElement)
{
if (currentElement is CodeMethod codeMethod
&& codeMethod.IsOfKind(CodeMethodKind.RequestGenerator, CodeMethodKind.RequestExecutor))
Expand All @@ -371,7 +567,7 @@

}

CrawlTree(currentElement, FlattenGoParamsFileNames);
CrawlTree(currentElement, FlattenParamsFileNames);
}

private List<string> getPathsName(CodeElement codeClass, string fileName, bool removeDuplicate = true)
Expand All @@ -388,7 +584,7 @@
.ToList();

// check if the last element contains current name and remove it
if (namespacePathSegments.Count > 0 && removeDuplicate && fileName.ToFirstCharacterUpperCase().Contains(namespacePathSegments.Last(), StringComparison.Ordinal))

Check warning on line 587 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Indexing at Count-1 should be used instead of the "Enumerable" extension method "Last" (https://rules.sonarsource.com/csharp/RSPEC-6608)
namespacePathSegments.RemoveAt(namespacePathSegments.Count - 1);

namespacePathSegments.Add(fileName.ToFirstCharacterUpperCase());
Expand All @@ -409,7 +605,7 @@
return namespaceList[^level];
}

private void FlattenGoFileNames(CodeElement currentElement)
private void FlattenFileNames(CodeElement currentElement)
{
// add the namespace to the name of the code element and the file name
if (currentElement is CodeClass codeClass
Expand All @@ -429,7 +625,7 @@
nextNameSpace.AddClass(codeClass);
}

CrawlTree(currentElement, FlattenGoFileNames);
CrawlTree(currentElement, FlattenFileNames);
}

private static void FixConstructorClashes(CodeElement currentElement, Func<string, string> nameCorrection)
Expand Down Expand Up @@ -552,7 +748,7 @@
"UUID",
"Guid"
};
private static readonly AdditionalUsingEvaluator[] defaultUsingEvaluators = {

Check warning on line 751 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this field to reduce its Cognitive Complexity from 27 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
new (static x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.RequestAdapter),
AbstractionsNamespaceName, "RequestAdapter"),
new (static x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestGenerator),
Expand Down Expand Up @@ -608,12 +804,12 @@
private const string SerializationNamespaceName = "github.com/microsoft/kiota-abstractions-go/serialization";
internal const string UntypedNodeName = "UntypedNodeable";

private void CorrectImplements(ProprietableBlockDeclaration block)

Check warning on line 807 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'CorrectImplements' a static method. (https://rules.sonarsource.com/csharp/RSPEC-2325)
{
block.ReplaceImplementByName(KiotaBuilder.AdditionalHolderInterface, "AdditionalDataHolder");
block.ReplaceImplementByName(KiotaBuilder.BackedModelInterface, "BackedModel");
}
private static void CorrectMethodType(CodeMethod currentMethod)

Check warning on line 812 in src/Kiota.Builder/Refiners/GoRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
var parentClass = currentMethod.Parent as CodeClass;
if (currentMethod.IsOfKind(CodeMethodKind.RequestGenerator, CodeMethodKind.RequestExecutor))
Expand Down
Loading
Loading