Skip to content

Commit 9b2a7ad

Browse files
committed
Prototype Scriban-based code generation
1 parent 3ce8d35 commit 9b2a7ad

11 files changed

Lines changed: 6734 additions & 7 deletions

File tree

GeneratedSchemaLibraries/Microsoft Search/Microsoft.Search.Query.xsd.cs

Lines changed: 5733 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#nullable enable
2+
using System.CodeDom;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Reflection;
6+
using System.Text.RegularExpressions;
7+
using Scriban;
8+
9+
namespace Xml.Schema.Linq.CodeGen.Scriban;
10+
11+
static class ScribanGlobals
12+
{
13+
public static void Comments(TemplateContext ctx, object target)
14+
{
15+
var comments = target switch
16+
{
17+
CodeTypeDeclaration decl => decl.Comments,
18+
CodeTypeMember m => m.Comments,
19+
_ => null,
20+
};
21+
22+
if (comments == null || comments.Count == 0)
23+
return;
24+
25+
foreach (CodeCommentStatement c in comments)
26+
ctx.Write($"/// {c.Comment.Text.Replace("\n", "\n///")}\n");
27+
ctx.ResetPreviousNewLine();
28+
ctx.Write(ctx.CurrentIndent);
29+
}
30+
31+
public static IEnumerable<CodeTypeDeclaration> Classes(CodeTypeDeclaration type)
32+
{
33+
return type.Members
34+
.OfType<CodeTypeDeclaration>()
35+
.Where(x => !x.IsEnum && !x.Name.EndsWith("EnumValidator"));
36+
}
37+
38+
public static CodeTypeDeclaration EnumDecl(string enumName, CodeTypeDeclaration type)
39+
{
40+
enumName = enumName.TrimEnd('?');
41+
return type.Members
42+
.OfType<CodeTypeDeclaration>()
43+
.First(x => x.IsEnum && x.Name == enumName);
44+
}
45+
46+
public static CodeConstructor? Ctor(CodeTypeDeclaration type, int args = 0)
47+
{
48+
return type.Members
49+
.OfType<CodeConstructor>()
50+
.FirstOrDefault(x => x.Parameters.Count == args);
51+
}
52+
53+
public static IEnumerable<CodeMemberProperty> Properties(CodeTypeDeclaration type)
54+
{
55+
return type.Members
56+
.OfType<CodeMemberProperty>()
57+
.Where(x =>
58+
// Exclude properties like SchemaName, TypeOrigin or TypeManager
59+
!x.CustomAttributes.Cast<CodeAttributeDeclaration>().Any(a => a.Name == "DebuggerBrowsable") &&
60+
x.Name != "TypedValue");
61+
}
62+
63+
public static bool IsList(CodeMemberProperty prop)
64+
{
65+
return prop.Type.BaseType == "IList`1";
66+
}
67+
68+
public static bool IsElement(CodeMemberProperty prop)
69+
{
70+
return IsList(prop) ||
71+
prop.GetStatements[0] is CodeVariableDeclarationStatement { Type.BaseType: "XElement" };
72+
}
73+
74+
public static IEnumerable<CodeMemberProperty> Elements(CodeTypeDeclaration type)
75+
{
76+
return Properties(type).Where(IsElement);
77+
}
78+
79+
public static bool HasElements(CodeTypeDeclaration type)
80+
{
81+
return Elements(type).Any();
82+
}
83+
84+
public static bool IsOptional(CodeMemberProperty prop)
85+
{
86+
return prop.Comments
87+
.Cast<CodeCommentStatement>()
88+
.Any(x => x.Comment.Text.Contains("Occurrence: optional"));
89+
}
90+
91+
public static bool IsTypeDefinition(CodeTypeDeclaration type)
92+
{
93+
return type.TypeAttributes.HasFlag(TypeAttributes.Sealed);
94+
}
95+
96+
public static string? Validator(CodeTypeDeclaration type)
97+
{
98+
var statement = type.Members
99+
.OfType<CodeMemberProperty>()
100+
.First(x => x.Name == "TypedValue")
101+
.SetStatements[0];
102+
103+
if (statement is not CodeExpressionStatement
104+
{
105+
Expression: CodeMethodInvokeExpression
106+
{
107+
Method.MethodName: "SetValueWithValidation",
108+
Parameters: [_, _, CodeFieldReferenceExpression
109+
{
110+
TargetObject: CodeTypeReferenceExpression
111+
{
112+
Type: var typeRef
113+
}
114+
}]
115+
}
116+
})
117+
return null;
118+
119+
return typeRef.BaseType;
120+
}
121+
122+
public static string? SimpleType(CodeTypeDeclaration type)
123+
{
124+
var typeDecl = type.Members
125+
.OfType<CodeMemberProperty>()
126+
.FirstOrDefault(x => x.Name == "TypedValue")
127+
?.Type;
128+
return typeDecl != null ? TypeName(typeDecl, nullable: false) : null;
129+
}
130+
131+
public static IEnumerable<string> EnumValues(CodeTypeDeclaration enumType)
132+
{
133+
return enumType.Members
134+
.OfType<CodeMemberField>()
135+
.Select(x => x.Name);
136+
}
137+
138+
public static string? DefaultValue(CodeMemberProperty prop, CodeTypeDeclaration type)
139+
{
140+
var name = prop.Name + "DefaultValue";
141+
var init = type.Members
142+
.OfType<CodeMemberField>()
143+
.FirstOrDefault(x => x.Name == name)
144+
?.InitExpression;
145+
146+
return init switch
147+
{
148+
CodeMethodInvokeExpression i => $"{i.Method.MethodName}(\"{ ((CodePrimitiveExpression)i.Parameters[0]).Value }\")",
149+
CodePrimitiveExpression p => (string)p.Value,
150+
CodeFieldReferenceExpression f => f.FieldName,
151+
_ => null,
152+
};
153+
}
154+
155+
public static string LocalName(CodeTypeDeclaration type, string name)
156+
{
157+
var init = type.Members
158+
.OfType<CodeMemberField>()
159+
.FirstOrDefault(x => x.Name == name)
160+
?.InitExpression as CodeMethodInvokeExpression;
161+
if (init is null) return "TODO: review null";
162+
return (string)(init.Parameters[0] as CodePrimitiveExpression)!.Value;
163+
}
164+
165+
public static string Namespace(CodeTypeDeclaration type, string name)
166+
{
167+
var init = type.Members
168+
.OfType<CodeMemberField>()
169+
.FirstOrDefault(x => x.Name == name)
170+
?.InitExpression as CodeMethodInvokeExpression;
171+
if (init is null) return "TODO: review null";
172+
return (string)(init.Parameters[1] as CodePrimitiveExpression)!.Value;
173+
}
174+
175+
public static bool HasContentModel(CodeTypeDeclaration type)
176+
{
177+
return type.Members
178+
.OfType<CodeMemberField>()
179+
.Any(x => x.Name == "contentModel");
180+
}
181+
182+
public static string TypeName(CodeTypeReference type, bool nullable = true)
183+
{
184+
if (type.ArrayElementType != null)
185+
return TypeName(type.ArrayElementType) + "[]";
186+
187+
var name = type.BaseType;
188+
189+
if (type.TypeArguments.Count > 0)
190+
{
191+
return Regex.Replace(name, @"`\d+$", "")
192+
+ "<"
193+
+ string.Join(", ", type.TypeArguments.Cast<CodeTypeReference>().Select(x => TypeName(x)))
194+
+ ">";
195+
}
196+
197+
if (!nullable)
198+
name = name.TrimEnd('?');
199+
200+
return name switch
201+
{
202+
"System.Boolean" => "bool",
203+
"System.Byte" => "byte",
204+
"System.Int32" => "int",
205+
"System.String" => "string",
206+
_ => name,
207+
};
208+
}
209+
210+
public static string ListElement(string typeName)
211+
{
212+
return Regex.Match(typeName, "^IList<([^>]+)>$") is { Success: true, Groups: var g }
213+
? g[1].Value
214+
: typeName;
215+
}
216+
217+
public static string XmlTypeCode(object type, string? name = null)
218+
{
219+
// HACK: this is plain wrong but a shortcut to make the proof of concept match 100% without going to deep in CodeDom analysis
220+
if (name == "language") return "XmlTypeCode.Language";
221+
222+
if (type is CodeTypeReference typeRef)
223+
type = TypeName(typeRef, nullable: false);
224+
225+
return type switch
226+
{
227+
"bool" => "XmlTypeCode.Boolean",
228+
"byte[]" => "XmlTypeCode.Base64Binary",
229+
"int" => "XmlTypeCode.Int",
230+
"string" => "XmlTypeCode.String",
231+
_ => "TODO",
232+
};
233+
}
234+
235+
public static IEnumerable<CodeTypeDeclaration> AllTypes(CodeNamespace ns)
236+
{
237+
return ns.Types
238+
.Cast<CodeTypeDeclaration>()
239+
.Where(x => x.Name is not ("XRootNamespace" or "XRoot" or "LinqToXsdTypeManager"));
240+
}
241+
242+
public static IEnumerable<CodeTypeDeclaration> ElementTypes(CodeNamespace ns)
243+
{
244+
return AllTypes(ns).Where(x => !IsTypeDefinition(x));
245+
}
246+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Scriban;
2+
using Scriban.Parsing;
3+
using Scriban.Runtime;
4+
using System.IO;
5+
using System.Threading.Tasks;
6+
7+
namespace Xml.Schema.Linq.CodeGen.Scriban;
8+
9+
class TemplateLoader : ITemplateLoader
10+
{
11+
static string FullPath(string name)
12+
{
13+
return Path.Combine(
14+
Path.GetDirectoryName(typeof(TemplateLoader).Assembly.Location),
15+
"Templates",
16+
name);
17+
}
18+
19+
public static Template Load(string name)
20+
{
21+
var file = FullPath(name);
22+
return Template.Parse(File.ReadAllText(file), file);
23+
}
24+
25+
public string GetPath(TemplateContext context, SourceSpan callerSpan, string templateName)
26+
=> FullPath(templateName);
27+
28+
public string Load(TemplateContext context, SourceSpan callerSpan, string templatePath)
29+
=> File.ReadAllText(templatePath);
30+
31+
public ValueTask<string> LoadAsync(TemplateContext context, SourceSpan callerSpan, string templatePath)
32+
=> new ValueTask<string>(File.ReadAllText(templatePath));
33+
}

XObjectsCode/Src/XObjectsCoreGenerator.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
using System.Collections.Generic;
44
using System.IO;
55
using System.Linq;
6+
using System.Text;
67
using System.Xml;
78
using System.Xml.Linq;
89
using System.Xml.Schema;
10+
using Scriban;
11+
using Scriban.Runtime;
912
using Xml.Schema.Linq.CodeGen;
13+
using Xml.Schema.Linq.CodeGen.Scriban;
1014
using Xml.Schema.Linq.Extensions;
1115
using XObjects;
1216

@@ -143,8 +147,9 @@ public static Dictionary<string, TextWriter> Generate(IEnumerable<string> xsdFil
143147
return GenerateCodeCompileUnits(schemaSet, settings)
144148
.Select(x =>
145149
{
146-
var writer = x.unit.ToStringWriter();
150+
var writer = new StringWriter(new StringBuilder(x.code));
147151

152+
// TODO: put directly in template
148153
if (settings.NullableReferences)
149154
{
150155
// HACK: CodeDom doesn't allow us to add #pragmas.
@@ -168,7 +173,7 @@ public static Dictionary<string, TextWriter> Generate(IEnumerable<string> xsdFil
168173
/// </returns>
169174
/// <exception cref="T:System.ArgumentNullException"><paramref name="schemaSet"/> is <see langword="null"/></exception>
170175
/// <exception cref="T:System.ArgumentNullException"><paramref name="settings"/> is <see langword="null"/></exception>
171-
public static IEnumerable<(string clrNamespace, CodeCompileUnit unit)> GenerateCodeCompileUnits(XmlSchemaSet schemaSet, LinqToXsdSettings settings)
176+
public static IEnumerable<(string clrNamespace, string code)> GenerateCodeCompileUnits(XmlSchemaSet schemaSet, LinqToXsdSettings settings)
172177
{
173178
if (schemaSet == null) throw new ArgumentNullException(nameof(schemaSet));
174179
if (settings == null) throw new ArgumentNullException(nameof(settings));
@@ -178,16 +183,29 @@ public static Dictionary<string, TextWriter> Generate(IEnumerable<string> xsdFil
178183
var codeGenerator = new CodeDomTypesGenerator(settings);
179184
var namespaces = codeGenerator.GenerateTypes(mapping);
180185

186+
var template = TemplateLoader.Load("file.scriban-cs");
187+
181188
return settings.SplitFilesByNamespace
182189
? namespaces.GroupBy(ns => ns.Name).Select(g => (g.Key, BuildUnit(g)))
183190
: new[] { ((string)null, BuildUnit(namespaces)) };
184191

185-
static CodeCompileUnit BuildUnit(IEnumerable<CodeNamespace> namespaces)
192+
// TODO: rename
193+
string BuildUnit(IEnumerable<CodeNamespace> namespaces)
186194
{
187-
var ccu = new CodeCompileUnit();
188-
foreach (var ns in namespaces)
189-
ccu.Namespaces.Add(ns);
190-
return ccu;
195+
var globals = new ScriptObject();
196+
globals.Import(typeof(ScribanGlobals));
197+
globals.Import(
198+
new { Settings = settings, Namespaces = namespaces.ToArray() },
199+
renamer: m => m.Name);
200+
201+
var context = new TemplateContext()
202+
{
203+
MemberRenamer = m => m.Name,
204+
TemplateLoader = new TemplateLoader(),
205+
};
206+
context.PushGlobal(globals);
207+
208+
return template.Render(context);
191209
}
192210
}
193211

0 commit comments

Comments
 (0)