Skip to content

Commit 267efd5

Browse files
committed
Prototype Scriban-based code generation
1 parent 5bb8696 commit 267efd5

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
@@ -4,10 +4,14 @@
44
using System.Collections.Generic;
55
using System.IO;
66
using System.Linq;
7+
using System.Text;
78
using System.Xml;
89
using System.Xml.Linq;
910
using System.Xml.Schema;
11+
using Scriban;
12+
using Scriban.Runtime;
1013
using Xml.Schema.Linq.CodeGen;
14+
using Xml.Schema.Linq.CodeGen.Scriban;
1115
using Xml.Schema.Linq.Extensions;
1216
using XObjects;
1317

@@ -146,8 +150,9 @@ public static Dictionary<string, TextWriter> Generate(IEnumerable<string> xsdFil
146150
return GenerateCodeCompileUnits(schemaSet, settings)
147151
.Select(x =>
148152
{
149-
var writer = x.unit.ToStringWriter();
153+
var writer = new StringWriter(new StringBuilder(x.code));
150154

155+
// TODO: put directly in template
151156
if (settings.NullableReferences)
152157
{
153158
// HACK: CodeDom doesn't allow us to add #pragmas.
@@ -171,7 +176,7 @@ public static Dictionary<string, TextWriter> Generate(IEnumerable<string> xsdFil
171176
/// </returns>
172177
/// <exception cref="T:System.ArgumentNullException"><paramref name="schemaSet"/> is <see langword="null"/></exception>
173178
/// <exception cref="T:System.ArgumentNullException"><paramref name="settings"/> is <see langword="null"/></exception>
174-
public static IEnumerable<(string clrNamespace, CodeCompileUnit unit)> GenerateCodeCompileUnits(XmlSchemaSet schemaSet, LinqToXsdSettings settings)
179+
public static IEnumerable<(string clrNamespace, string code)> GenerateCodeCompileUnits(XmlSchemaSet schemaSet, LinqToXsdSettings settings)
175180
{
176181
if (schemaSet == null) throw new ArgumentNullException(nameof(schemaSet));
177182
if (settings == null) throw new ArgumentNullException(nameof(settings));
@@ -181,16 +186,29 @@ public static Dictionary<string, TextWriter> Generate(IEnumerable<string> xsdFil
181186
var codeGenerator = new CodeDomTypesGenerator(settings);
182187
var namespaces = codeGenerator.GenerateTypes(mapping);
183188

189+
var template = TemplateLoader.Load("file.scriban-cs");
190+
184191
return settings.SplitFilesByNamespace
185192
? namespaces.GroupBy(ns => ns.Name).Select(g => (g.Key, BuildUnit(g)))
186193
: new[] { ((string)null, BuildUnit(namespaces)) };
187194

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

0 commit comments

Comments
 (0)