Skip to content

Instantly share code, notes, and snippets.

@tintoy
Last active April 29, 2018 06:00
Show Gist options
  • Save tintoy/93a8f50fb3f4dafcfa835cbeb804b441 to your computer and use it in GitHub Desktop.
Save tintoy/93a8f50fb3f4dafcfa835cbeb804b441 to your computer and use it in GitHub Desktop.
Kubernetes model-transform to add base classes
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Buildalyzer" Version="0.4.0" />
<PackageReference Include="Buildalyzer.Workspaces" Version="0.4.0" />
</ItemGroup>
<!--
A little ugly: to make Buildalyzer work with the .NET Core 2.1-preview2 SDK,
we have to include some specific dependency versions since the MSBuild engine is loaded in-process (and requires these assemblies at build-time).
-->
<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="15.6.85" NoWarn="NU1701" />
<PackageReference Include="Microsoft.Build.Engine" Version="15.6.85" NoWarn="NU1701" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="2.6.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.6.1" />
<PackageReference Include="NuGet.Common" Version="4.7.0-preview1-4986" />
<PackageReference Include="NuGet.Frameworks" Version="4.7.0-preview1-4986" />
<PackageReference Include="NuGet.ProjectModel" Version="4.7.0-preview1-4986" />
</ItemGroup>
</Project>
using Buildalyzer;
using Buildalyzer.Workspaces;
using Microsoft.Build.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Task = System.Threading.Tasks.Task;
namespace K8sModelTransform
{
class Program
{
static async Task Main()
{
const string solutionFile = @"D:\Development\github\kubernetes-client\csharp\kubernetes-client.sln";
const string projectFile = @"D:\Development\github\kubernetes-client\csharp\src\KubernetesClient\KubernetesClient.csproj";
try
{
AnalyzerManager solutionAnalyzer = new AnalyzerManager(solutionFile);
// Multi-targeting means we need to explicitly nominate a target framework.
//
// Since we're running on .NET Core, we only want to build for .NET Standard.
ProjectAnalyzer clientProjectAnalyzer = solutionAnalyzer.GetProject(projectFile);
clientProjectAnalyzer.SetGlobalProperty("TargetFramework",
value: "netstandard1.4"
);
using (AdhocWorkspace workspace = solutionAnalyzer.GetWorkspace())
{
Project clientProject = workspace.CurrentSolution.Projects.First(project => project.Name == "KubernetesClient");
CSharpCompilation compilation = (CSharpCompilation)await clientProject.GetCompilationAsync();
foreach (SyntaxTree syntaxTree in compilation.SyntaxTrees)
{
SemanticModel semanticModel = compilation.GetSemanticModel(syntaxTree);
SyntaxNode syntaxRoot = await syntaxTree.GetRootAsync();
foreach (ClassDeclarationSyntax classDeclaration in syntaxRoot.DescendantNodesAndSelf().OfType<ClassDeclarationSyntax>())
{
INamedTypeSymbol classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
if (classSymbol.ContainingNamespace.Name != "Models")
continue;
// Skip over JsonConverters - we only want to deal with models.
if (classSymbol.BaseType.Name == "JsonConverter")
continue;
// Skip over any classes that already declare a base class.
if (classSymbol.BaseType.Name != "Object")
continue;
IPropertySymbol kindProperty = null, apiVersionProperty = null, metadataProperty = null, itemsProperty = null;
foreach (IPropertySymbol property in classSymbol.GetMembers().Where(member => member.Kind == SymbolKind.Property))
{
switch (property.Name)
{
case "Kind":
{
kindProperty = property;
break;
}
case "ApiVersion":
{
apiVersionProperty = property;
break;
}
case "Metadata":
{
metadataProperty = property;
break;
}
case "Items":
{
itemsProperty = property;
break;
}
}
}
if (kindProperty == null || apiVersionProperty == null || metadataProperty == null)
continue;
string metadataType = metadataProperty.GetMethod.ReturnType.Name;
if (metadataType == "V1ObjectMeta")
{
await File.WriteAllTextAsync(syntaxTree.FilePath,
new ChangeBaseClass(classDeclaration, "KubeResourceV1").Visit(syntaxRoot).ToString()
);
}
else if (metadataType == "V1ListMeta")
{
await File.WriteAllTextAsync(syntaxTree.FilePath,
new ChangeBaseClass(classDeclaration, "KubeResourceListV1").Visit(syntaxRoot).ToString()
);
}
}
}
}
}
catch (Exception unexpectedError)
{
Console.WriteLine(unexpectedError);
}
}
class ChangeBaseClass
: Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter
{
readonly ClassDeclarationSyntax _replaceClass;
readonly string _baseTypeName;
public ChangeBaseClass(ClassDeclarationSyntax replaceClass, string baseTypeName)
{
if (replaceClass == null)
throw new ArgumentNullException(nameof(replaceClass));
if (String.IsNullOrWhiteSpace(baseTypeName))
throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'baseTypeName'.", nameof(baseTypeName));
_replaceClass = replaceClass;
_baseTypeName = baseTypeName;
}
public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax classDeclaration)
{
if (!ReferenceEquals(classDeclaration, _replaceClass))
return classDeclaration;
// Declare base class.
var updatedClassDeclaration = classDeclaration
.WithIdentifier(
classDeclaration.Identifier.WithoutTrivia() // Strip off newline
)
.WithBaseList(
SyntaxFactory.BaseList(
colonToken: SyntaxFactory.ParseToken(" : "),
types: SyntaxFactory.SeparatedList<BaseTypeSyntax>(new []
{
SyntaxFactory.SimpleBaseType(
SyntaxFactory.ParseTypeName(_baseTypeName)
)
})
)
.WithTrailingTrivia(
SyntaxFactory.TriviaList(
SyntaxFactory.ParseTrailingTrivia("\r\n")
)
)
);
// Because we're changing the class in a loop, we need to track old vs new node identities for the properties we're removing.
PropertyDeclarationSyntax[] properties = updatedClassDeclaration.DescendantNodes().OfType<PropertyDeclarationSyntax>().ToArray();
updatedClassDeclaration = updatedClassDeclaration.TrackNodes(properties);
foreach (PropertyDeclarationSyntax property in properties)
{
switch (property.Identifier.Text)
{
case "Kind":
case "ApiVersion":
case "Metadata":
{
// The PropertyDeclarationSyntax instance for the current instance of the class definition.
PropertyDeclarationSyntax currentProperty = updatedClassDeclaration.GetCurrentNode(property);
updatedClassDeclaration = updatedClassDeclaration.RemoveNode(currentProperty, SyntaxRemoveOptions.KeepNoTrivia);
break;
}
case "Items":
{
if (_baseTypeName == "KubeResourceListV1")
{
// The PropertyDeclarationSyntax instance for the current instance of the class definition.
PropertyDeclarationSyntax currentProperty = updatedClassDeclaration.GetCurrentNode(property);
updatedClassDeclaration = updatedClassDeclaration.RemoveNode(currentProperty, SyntaxRemoveOptions.KeepNoTrivia);
}
break;
}
}
}
return base.VisitClassDeclaration(updatedClassDeclaration);
}
public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax constructorDeclaration)
{
if (constructorDeclaration == null)
throw new ArgumentNullException(nameof(constructorDeclaration));
// Ignore the default constructor.
if (!constructorDeclaration.ParameterList.ChildNodes().Any())
return constructorDeclaration;
// Strip out parameters corresponding to base class properties.
constructorDeclaration = constructorDeclaration.WithParameterList(
constructorDeclaration.ParameterList.RemoveNodes(
constructorDeclaration.ParameterList.Parameters.Where(parameter =>
{
switch (parameter.Identifier.Text)
{
case "kind":
case "apiVersion":
case "metadata":
{
return true;
}
}
return false;
}),
SyntaxRemoveOptions.KeepNoTrivia
)
);
// Strip out property assignments corresponding to base class properties.
constructorDeclaration = constructorDeclaration.WithBody(
constructorDeclaration.Body.RemoveNodes(constructorDeclaration.Body.Statements.Where(statement =>
{
if (statement is ExpressionStatementSyntax expressionStatement)
{
if (expressionStatement.Expression is AssignmentExpressionSyntax assignment)
{
if (assignment.Left is IdentifierNameSyntax identifierName)
{
switch (identifierName.Identifier.Text)
{
case "Kind":
case "ApiVersion":
case "Metadata":
{
return true;
}
}
}
}
}
return false;
}),
SyntaxRemoveOptions.KeepNoTrivia
));
return constructorDeclaration;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment