Skip to content

Commit

Permalink
Support complex namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
NikolayPianikov authored and NikolayPianikov committed Jan 21, 2022
1 parent 679e534 commit 9368e46
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Immutype.Tests/Immutype.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<ProjectReference Include="..\Immutype\Immutype.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
94 changes: 91 additions & 3 deletions Immutype.Tests/Integration/Tests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Shouldly;
// ReSharper disable StringLiteralTypo
using Shouldly;
using Xunit;
// ReSharper disable StringLiteralTypo

namespace Immutype.Tests.Integration
{
Expand Down Expand Up @@ -228,7 +228,7 @@ public record Rec(IEnumerable<int> vals, int val);
// Then
output.ShouldBe(new [] { "33,77,99" }, generatedCode);
}

[Fact]
public void ShouldCreateRecordAddSingleIReadOnlyCollection()
{
Expand Down Expand Up @@ -944,5 +944,93 @@ public record Rec(IImmutableList<int>? vals);
// Then
output.ShouldBe(new [] { "55,66,99,44" }, generatedCode);
}

[Fact]
public void ShouldSupportPartialUsing()
{
// Given
const string statements = "System.Console.WriteLine(new Rec(new Types.Rec2()).WithVal(new Types.Rec2()));";

// When
var output = @"
using System.Collections.Generic;
namespace Sample
{
using System;
using Types;
using System.Collections.Generic;
[Immutype.TargetAttribute()]
public record Rec(Rec2 val);
}
namespace Sample.Types
{
using System;
[Immutype.TargetAttribute()]
public record Rec2();
}
".Run(out var generatedCode, new RunOptions { Statements = statements });

// Then
output.ShouldBe(new [] { "Rec { val = Rec2 { } }" }, generatedCode);
}

[Fact]
public void ShouldSupportTopLevelUsing()
{
// Given
const string statements = "System.Console.WriteLine(new Rec(new Types.Rec2(), new System.Text.StringBuilder()).WithVal(new Types.Rec2()));";

// When
var output = @"
using System.Text;
namespace Sample
{
using Types;
using System.Collections.Generic;
[Immutype.TargetAttribute()]
public record Rec(Rec2 val, StringBuilder stringBuilder);
}
namespace Sample.Types
{
using System;
[Immutype.TargetAttribute()]
public record Rec2();
}
".Run(out var generatedCode, new RunOptions { Statements = statements });

// Then
output.ShouldBe(new [] { "Rec { val = Rec2 { }, stringBuilder = }" }, generatedCode);
}

[Fact]
public void ShouldSupportEmptyNamespace()
{
// Given
const string statements = "System.Console.WriteLine(new Rec(new Types.Rec2()).WithVal(new Types.Rec2()));";

// When
var output = @"
using System;
using Types;
using System.Collections.Generic;
[Immutype.TargetAttribute()]
public record Rec(Types.Rec2 val);
namespace Types
{
[Immutype.TargetAttribute()]
public record Rec2();
}
".Run(out var generatedCode, new RunOptions { Statements = statements });

// Then
output.ShouldBe(new [] { "Rec { val = Rec2 { } }" }, generatedCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<ItemGroup>
<CompilerVisibleProperty Include="ImmutypeAPI"/>
<ProjectReference Include="..\Immutype\Immutype.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
7 changes: 6 additions & 1 deletion Immutype.UsageScenarios.Tests/README_TEMPLATE.tt
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@
foreach(var info in group.OrderBy(i => i["priority"] + "_" + i["description"]))
{
var description = info["description"];
var reference = "#" + description.Replace(" ", "-").Replace("'", string.Empty).ToLowerInvariant();
var reference = "#" + descriptiondescription.Replace(" ", "-")
.Replace("'", string.Empty)
.Replace("/", string.Empty)
.Replace("`", string.Empty)
.Replace("\\", string.Empty)
.ToLowerInvariant();
#> - [<#=description#>](<#=reference#>)
<#
}
Expand Down
68 changes: 38 additions & 30 deletions Immutype/Core/ExtensionsFactory.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable LoopCanBeConvertedToQuery
// ReSharper disable IdentifierTypo
namespace Immutype.Core
{
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
#if ROSLYN38
using NamespaceType = Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax;
#else
using NamespaceType = Microsoft.CodeAnalysis.CSharp.Syntax.BaseNamespaceDeclarationSyntax;
#endif

internal class ExtensionsFactory : IUnitFactory
{
private static readonly UsingDirectiveSyntax[] AdditionalUsings = {
SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Collections.Generic")),
SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Linq"))
};

private readonly IMethodsFactory _methodsFactory;

public ExtensionsFactory(IMethodsFactory methodsFactory) =>
Expand Down Expand Up @@ -39,40 +51,36 @@ public IEnumerable<Source> Create(GenerationContext<TypeDeclarationSyntax> conte
.AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword))
.AddMembers(_methodsFactory.Create(context, typeSyntax, parameters).ToArray());

var usingDirectives = typeDeclarationSyntax.SyntaxTree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>()
.Concat(new []
{
SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Collections.Generic")),
SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Linq"))
})
.GroupBy(i => i.Name.ToString())
.Select(i => i.First())
.ToArray();

var compilationUnit = SyntaxFactory.CompilationUnit().AddUsings(usingDirectives);
NamespaceDeclarationSyntax? prevNamespaceNode = null;
foreach (var originalNamespaceNode in ns.Reverse())
{
var namespaceNode =
SyntaxFactory.NamespaceDeclaration(originalNamespaceNode.Name)
.AddUsings(originalNamespaceNode.Usings.ToArray());
var code = CreateRootNode(typeDeclarationSyntax, AdditionalUsings, extensionsClass).NormalizeWhitespace().ToString();
var fileName = string.Join(".", ns.Select(i => i.Name.ToString()).Concat(new []{typeDeclarationSyntax.Identifier.Text}));
yield return new Source(fileName, SourceText.From(code, Encoding.UTF8));
}

prevNamespaceNode = prevNamespaceNode == null ? namespaceNode : prevNamespaceNode.AddMembers(namespaceNode);
}

if (prevNamespaceNode != null)
{
prevNamespaceNode = prevNamespaceNode.AddMembers(extensionsClass);
compilationUnit = compilationUnit.AddMembers(prevNamespaceNode);
}
else
private static CompilationUnitSyntax CreateRootNode(SyntaxNode targetNode, UsingDirectiveSyntax[] additionalUsings, params MemberDeclarationSyntax[] members)
{
var namespaces = targetNode.Ancestors().OfType<NamespaceType>();
NamespaceType? rootNamespace = default;
foreach (var ns in namespaces)
{
compilationUnit = compilationUnit.AddMembers(extensionsClass);
var nextNs = ns.WithMembers(new SyntaxList<MemberDeclarationSyntax>(Enumerable.Empty<MemberDeclarationSyntax>()));
rootNamespace = rootNamespace == default
? nextNs.AddMembers(members).AddUsings(GetUsings(nextNs.Usings, additionalUsings))
: nextNs.AddMembers(rootNamespace);
}

var baseCompilationUnit = targetNode.Ancestors().OfType<CompilationUnitSyntax>().FirstOrDefault();
var rootCompilationUnit = (baseCompilationUnit ?? SyntaxFactory.CompilationUnit())
.WithMembers(new SyntaxList<MemberDeclarationSyntax>(Enumerable.Empty<MemberDeclarationSyntax>()));

var code = compilationUnit.NormalizeWhitespace().ToString();
var fileName = string.Join(".", ns.Select(i => i.Name.ToString()).Concat(new []{typeDeclarationSyntax.Identifier.Text}));
yield return new Source(fileName, SourceText.From(code, Encoding.UTF8));
return rootNamespace != default
? rootCompilationUnit.AddMembers(rootNamespace)
: rootCompilationUnit.AddUsings(GetUsings(rootCompilationUnit.Usings, additionalUsings)).AddMembers(members);
}

private static UsingDirectiveSyntax[] GetUsings(IEnumerable<UsingDirectiveSyntax> usings, IEnumerable<UsingDirectiveSyntax> additionalUsings)
{
var currentUsins = usings.Select(i => i.Name.ToString()).ToImmutableHashSet();
return additionalUsings.Where(i => !currentUsins.Contains(i.Name.ToString())).ToArray();
}
}
}
2 changes: 1 addition & 1 deletion Immutype/Immutype.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<!-- Should be compatible with .NET 5.0.102+ and Visual Studio 16.8+ -->
<!-- https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md#versioning -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(AnalyzerRoslynPackageVersion)" PrivateAssets="all" />
<PackageReference Include="Pure.DI" Version="1.1.18">
<PackageReference Include="Pure.DI" Version="1.1.21">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down

0 comments on commit 9368e46

Please sign in to comment.