Last active
April 29, 2018 06:00
-
-
Save tintoy/93a8f50fb3f4dafcfa835cbeb804b441 to your computer and use it in GitHub Desktop.
Kubernetes model-transform to add base classes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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