[NUI] Add file comment and end empty line
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.XamlBuild / src / public / XamlBuild / XamlGenerator.cs
1 /*
2  * Copyright(c) 2022 Samsung Electronics Co., Ltd.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  */
17 using System;
18 using System.CodeDom;
19 using System.CodeDom.Compiler;
20 using System.Collections.Generic;
21 using System.IO;
22 using System.Reflection;
23 using System.Xml;
24 using Microsoft.Build.Framework;
25 using Microsoft.Build.Utilities;
26 using Microsoft.CSharp;
27 using Mono.Cecil;
28 using Tizen.NUI.Binding;
29 using Tizen.NUI.Xaml;
30
31 namespace Tizen.NUI.Xaml.Build.Tasks
32 {
33     class XamlGenerator
34     {
35         internal XamlGenerator()
36         {
37         }
38
39         private class XmlnsInfo
40         {
41             public void Add(ModuleDefinition module, string nameSpace, int level)
42             {
43                 foreach (TypeDefinition type in module.Types)
44                 {
45                     if (type.Namespace == nameSpace
46                         &&
47                         type.IsPublic == true
48                         &&
49                         type.IsClass == true)
50                     {
51                         bool needUpdate = false;
52                         if (true == classNameToNameSpace.ContainsKey(type.Name))
53                         {
54                             NameSpaceInfo info = classNameToNameSpace[type.Name];
55
56                             if (level > info.level)
57                             {
58                                 needUpdate = true;
59                             }
60                         }
61                         else
62                         {
63                             needUpdate = true;
64                         }
65                         
66                         if (true == needUpdate)
67                         {
68                             classNameToNameSpace[type.Name] = new NameSpaceInfo(type.Namespace, level);
69                         }
70                     }
71                 }
72             }
73
74             public string GetNameSpace(string nameSpace)
75             {
76                 NameSpaceInfo ret;
77
78                 classNameToNameSpace.TryGetValue(nameSpace, out ret);
79
80                 return ret?.nameSpace;
81             }
82
83             private class NameSpaceInfo
84             {
85                 internal NameSpaceInfo(string nameSpace, int level)
86                 {
87                     this.nameSpace = nameSpace;
88                     this.level = level;
89                 }
90
91                 internal string nameSpace;
92                 internal int level;
93             }
94
95             private Dictionary<string, NameSpaceInfo> classNameToNameSpace = new Dictionary<string, NameSpaceInfo>();
96         }
97
98         static private Dictionary<string, XmlnsInfo> xmlnsNameToInfo = new Dictionary<string, XmlnsInfo>();
99
100         internal string ReferencePath
101         {
102             set
103             {
104                 if (!string.IsNullOrEmpty(value))
105                 {
106                     List<ModuleDefinition> assemblyList = new List<ModuleDefinition>();
107
108                     var paths = value.Replace("//", "/").Split(';');
109                     foreach (var p in paths)
110                     {
111                         ModuleDefinition module = ModuleDefinition.ReadModule(p);
112
113                         foreach (var attr in module.Assembly.CustomAttributes)
114                         {
115                             if (attr.AttributeType.FullName == "Tizen.NUI.XmlnsDefinitionAttribute")
116                             {
117                                 string xmlNamespace = attr.ConstructorArguments[0].Value as string;
118                                 string clrNamespace = attr.ConstructorArguments[1].Value as string;
119
120                                 int level = 0;
121                                 string assemblyName = module.Assembly.FullName;
122
123                                 if (true == attr.HasProperties)
124                                 {
125                                     foreach (var property in attr.Properties)
126                                     {
127                                         if ("Level" == property.Name)
128                                         {
129                                             level = int.Parse(property.Argument.Value.ToString());
130                                         }
131                                         if ("AssemblyName" == property.Name)
132                                         {
133                                             assemblyName = property.Argument.Value as string;
134                                         }
135                                     }
136                                 }
137
138                                 XmlnsInfo xmlsInfo = null;
139
140                                 if (xmlnsNameToInfo.ContainsKey(xmlNamespace))
141                                 {
142                                     xmlsInfo = xmlnsNameToInfo[xmlNamespace];
143                                 }
144                                 else
145                                 {
146                                     xmlsInfo = new XmlnsInfo();
147                                     xmlnsNameToInfo.Add(xmlNamespace, xmlsInfo);
148                                 }
149
150                                 xmlsInfo.Add(module, clrNamespace, level);
151                             }
152                         }
153                     }
154                 }
155             }
156         }
157
158         public XamlGenerator(
159             ITaskItem taskItem,
160             string language,
161             string assemblyName,
162             string outputFile,
163             string ReferencePath,
164             TaskLoggingHelper logger)
165             : this(
166                 taskItem.ItemSpec,
167                 language,
168                 taskItem.GetMetadata("ManifestResourceName"),
169                 taskItem.GetMetadata("TargetPath"),
170                 assemblyName,
171                 outputFile,
172                 logger)
173         {
174             this.ReferencePath = ReferencePath;
175         }
176
177         static int generatedTypesCount;
178         internal static CodeDomProvider Provider = new CSharpCodeProvider();
179
180         public string XamlFile { get; }
181         public string Language { get; }
182         public string ResourceId { get; }
183         public string TargetPath { get; }
184         public string AssemblyName { get; }
185         public string OutputFile { get; }
186         public TaskLoggingHelper Logger { get; }
187         public string RootClrNamespace { get; private set; }
188         public string RootType { get; private set; }
189         public bool AddXamlCompilationAttribute { get; set; }
190         bool GenerateDefaultCtor { get; set; }
191         bool HideFromIntellisense { get; set; }
192         bool XamlResourceIdOnly { get; set; }
193         internal IEnumerable<CodeMemberField> NamedFields { get; set; }
194         internal CodeTypeReference BaseType { get; set; }
195
196         public XamlGenerator(
197             string xamlFile,
198             string language,
199             string resourceId,
200             string targetPath,
201             string assemblyName,
202             string outputFile,
203             TaskLoggingHelper logger = null)
204         {
205             XamlFile = xamlFile;
206             Language = language;
207             ResourceId = resourceId;
208             TargetPath = targetPath;
209             AssemblyName = assemblyName;
210             OutputFile = outputFile;
211             Logger = logger;
212         }
213
214         //returns true if a file is generated
215         public bool Execute()
216         {
217             Logger?.LogMessage(MessageImportance.Low, "Source: {0}", XamlFile);
218             Logger?.LogMessage(MessageImportance.Low, " Language: {0}", Language);
219             Logger?.LogMessage(MessageImportance.Low, " ResourceID: {0}", ResourceId);
220             Logger?.LogMessage(MessageImportance.Low, " TargetPath: {0}", TargetPath);
221             Logger?.LogMessage(MessageImportance.Low, " AssemblyName: {0}", AssemblyName);
222             Logger?.LogMessage(MessageImportance.Low, " OutputFile {0}", OutputFile);
223
224             using (StreamReader reader = File.OpenText(XamlFile))
225                 if (!ParseXaml(reader))
226                     return false;
227
228             GenerateCode(OutputFile);
229
230             return true;
231         }
232
233         internal bool ParseXaml(TextReader xaml)
234         {
235             var xmlDoc = new XmlDocument();
236             xmlDoc.Load(xaml);
237
238             // if the following xml processing instruction is present
239             //
240             // <?xaml-comp compile="true" ?>
241             //
242             // we will generate a xaml.g.cs file with the default ctor calling InitializeComponent, and a XamlCompilation attribute
243             var hasXamlCompilationProcessingInstruction = GetXamlCompilationProcessingInstruction(xmlDoc);
244
245             var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
246             nsmgr.AddNamespace("__f__", XamlParser.XFUri);
247
248             var root = xmlDoc.SelectSingleNode("/*", nsmgr);
249             if (root == null) {
250                 Logger?.LogMessage(MessageImportance.Low, " No root node found");
251                 return false;
252             }
253
254             foreach (XmlAttribute attr in root.Attributes) {
255                 if (attr.Name == "xmlns")
256                     nsmgr.AddNamespace("", attr.Value); //Add default xmlns
257                 if (attr.Prefix != "xmlns")
258                     continue;
259                 nsmgr.AddNamespace(attr.LocalName, attr.Value);
260             }
261
262             var rootClass = root.Attributes["Class", XamlParser.X2006Uri]
263                          ?? root.Attributes["Class", XamlParser.X2009Uri];
264
265             if (rootClass != null) {
266                 string rootType, rootNs, rootAsm, targetPlatform;
267                 XmlnsHelper.ParseXmlns(rootClass.Value, out rootType, out rootNs, out rootAsm, out targetPlatform);
268                 RootType = rootType;
269                 RootClrNamespace = rootNs;
270             }
271             else if (hasXamlCompilationProcessingInstruction) {
272                 RootClrNamespace = "__XamlGeneratedCode__";
273                 RootType = $"__Type{generatedTypesCount++}";
274                 GenerateDefaultCtor = true;
275                 AddXamlCompilationAttribute = true;
276                 HideFromIntellisense = true;
277             }
278             else { // rootClass == null && !hasXamlCompilationProcessingInstruction) {
279                 XamlResourceIdOnly = true; //only generate the XamlResourceId assembly attribute
280                 return true;
281             }
282
283             NamedFields = GetCodeMemberFields(root, nsmgr);
284             var typeArguments = GetAttributeValue(root, "TypeArguments", XamlParser.X2006Uri, XamlParser.X2009Uri);
285             var xmlType = new XmlType(root.NamespaceURI, root.LocalName, typeArguments != null ? TypeArgumentsParser.ParseExpression(typeArguments, nsmgr, null) : null);
286             BaseType = GetType(xmlType, root.GetNamespaceOfPrefix);
287
288             return true;
289         }
290
291         static System.Version version = typeof(XamlGenerator).Assembly.GetName().Version;
292         static CodeAttributeDeclaration GeneratedCodeAttrDecl =>
293             new CodeAttributeDeclaration(new CodeTypeReference($"global::{typeof(GeneratedCodeAttribute).FullName}"),
294                         new CodeAttributeArgument(new CodePrimitiveExpression("Tizen.NUI.Xaml.Build.Tasks.XamlG")),
295                         new CodeAttributeArgument(new CodePrimitiveExpression($"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}")));
296
297         void GenerateCode(string outFilePath)
298         {
299             //Create the target directory if required
300             Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outFilePath));
301
302             var ccu = new CodeCompileUnit();
303             ccu.AssemblyCustomAttributes.Add(
304                 new CodeAttributeDeclaration(new CodeTypeReference($"global::{typeof(XamlResourceIdAttribute).FullName}"),
305                                              new CodeAttributeArgument(new CodePrimitiveExpression(ResourceId)),
306                                              new CodeAttributeArgument(new CodePrimitiveExpression(TargetPath.Replace('\\', '/'))), //use forward slashes, paths are uris-like
307                                              new CodeAttributeArgument(RootType == null ? (CodeExpression)new CodePrimitiveExpression(null) : new CodeTypeOfExpression($"global::{RootClrNamespace}.{RootType}"))
308                                             ));
309             if (XamlResourceIdOnly)
310                 goto writeAndExit;
311
312             if (RootType == null)
313                 throw new Exception("Something went wrong while executing XamlG");
314
315             var declNs = new CodeNamespace(RootClrNamespace);
316             ccu.Namespaces.Add(declNs);
317
318             var declType = new CodeTypeDeclaration(RootType) {
319                 IsPartial = true,
320                 CustomAttributes = {
321                     new CodeAttributeDeclaration(new CodeTypeReference(XamlCTask.xamlNameSpace + ".XamlFilePathAttribute"),
322                          new CodeAttributeArgument(new CodePrimitiveExpression(XamlFile))),
323                 }
324             };
325             if (AddXamlCompilationAttribute)
326                 declType.CustomAttributes.Add(
327                     new CodeAttributeDeclaration(new CodeTypeReference(XamlCTask.xamlNameSpace + ".XamlCompilationAttribute"),
328                                                  new CodeAttributeArgument(new CodeSnippetExpression($"global::{typeof(XamlCompilationOptions).FullName}.Compile"))));
329             if (HideFromIntellisense)
330                 declType.CustomAttributes.Add(
331                     new CodeAttributeDeclaration(new CodeTypeReference($"global::{typeof(System.ComponentModel.EditorBrowsableAttribute).FullName}"),
332                                                  new CodeAttributeArgument(new CodeSnippetExpression($"global::{typeof(System.ComponentModel.EditorBrowsableState).FullName}.{nameof(System.ComponentModel.EditorBrowsableState.Never)}"))));
333
334             declType.BaseTypes.Add(BaseType);
335
336             declNs.Types.Add(declType);
337
338             //Create a default ctor calling InitializeComponent
339             if (GenerateDefaultCtor) {
340                 var ctor = new CodeConstructor {
341                     Attributes = MemberAttributes.Public,
342                     CustomAttributes = { GeneratedCodeAttrDecl },
343                     Statements = {
344                         new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "InitializeComponent")
345                     }
346                 };
347
348                 declType.Members.Add(ctor);
349             }
350
351             //Create InitializeComponent()
352             var initcomp = new CodeMemberMethod {
353                 Name = "InitializeComponent",
354                 CustomAttributes = { GeneratedCodeAttrDecl }
355             };
356
357             declType.Members.Add(initcomp);
358
359             //Create and initialize fields
360             var loadExaml_invoke = new CodeMethodInvokeExpression(
361                 new CodeTypeReferenceExpression(new CodeTypeReference($"global::Tizen.NUI.EXaml.EXamlExtensions")),
362                 "LoadFromEXamlByRelativePath", new CodeThisReferenceExpression(), 
363                 new CodeMethodInvokeExpression()
364                 { Method = new CodeMethodReferenceExpression() { MethodName = "GetEXamlPath" } });
365
366             CodeAssignStatement assignEXamlObject = new CodeAssignStatement(
367                     new CodeVariableReferenceExpression("eXamlData"), loadExaml_invoke);
368
369             initcomp.Statements.Add(assignEXamlObject);
370
371             foreach (var namedField in NamedFields) {
372                 declType.Members.Add(namedField);
373
374                 var find_invoke = new CodeMethodInvokeExpression(
375                     new CodeMethodReferenceExpression(
376                         new CodeTypeReferenceExpression(new CodeTypeReference($"global::Tizen.NUI.Binding.NameScopeExtensions")),
377                         "FindByName", namedField.Type),
378                     new CodeThisReferenceExpression(), new CodePrimitiveExpression(namedField.Name));
379
380                 CodeAssignStatement assign = new CodeAssignStatement(
381                     new CodeVariableReferenceExpression(namedField.Name), find_invoke);
382
383                 initcomp.Statements.Add(assign);
384             }
385
386             declType.Members.Add(new CodeMemberField
387             {
388                 Name = "eXamlData",
389                 Type = new CodeTypeReference("System.Object"),
390                 Attributes = MemberAttributes.Private,
391                 CustomAttributes = { GeneratedCodeAttrDecl }
392             });
393
394             var getEXamlPathcomp = new CodeMemberMethod()
395             {
396                 Name = "GetEXamlPath",
397                 ReturnType = new CodeTypeReference(typeof(string)),
398                 CustomAttributes = { GeneratedCodeAttrDecl }
399             };
400
401             getEXamlPathcomp.Statements.Add(new CodeMethodReturnStatement(new CodeDefaultValueExpression(new CodeTypeReference(typeof(string)))));
402
403             declType.Members.Add(getEXamlPathcomp);
404
405             GenerateMethodExitXaml(declType);
406
407         writeAndExit:
408             //write the result
409             using (var writer = new StreamWriter(outFilePath))
410                 Provider.GenerateCodeFromCompileUnit(ccu, writer, new CodeGeneratorOptions());
411         }
412
413         private static void GenerateMethodExitXaml(CodeTypeDeclaration declType)
414         {
415             var removeEventsComp = new CodeMemberMethod()
416             {
417                 Name = "RemoveEventsInXaml",
418                 CustomAttributes = { GeneratedCodeAttrDecl }
419             };
420
421             removeEventsComp.Statements.Add(new CodeMethodInvokeExpression(
422                 new CodeTypeReferenceExpression(new CodeTypeReference($"global::Tizen.NUI.EXaml.EXamlExtensions")),
423                 "RemoveEventsInXaml", new CodeVariableReferenceExpression("eXamlData")));
424
425             declType.Members.Add(removeEventsComp);
426
427             var exitXamlComp = new CodeMemberMethod()
428             {
429                 Name = "ExitXaml",
430                 CustomAttributes = { GeneratedCodeAttrDecl }
431             };
432
433             exitXamlComp.Statements.Add(new CodeMethodInvokeExpression(new CodeMethodReferenceExpression()
434             {
435                 MethodName = "RemoveEventsInXaml",
436             }));
437
438             var disposeXamlElements_invoke = new CodeMethodInvokeExpression(
439                 new CodeTypeReferenceExpression(new CodeTypeReference($"global::Tizen.NUI.EXaml.EXamlExtensions")),
440                 "DisposeXamlElements", new CodeThisReferenceExpression());
441
442             exitXamlComp.Statements.Add(disposeXamlElements_invoke);
443
444             declType.Members.Add(exitXamlComp);
445         }
446
447         static IEnumerable<CodeMemberField> GetCodeMemberFields(XmlNode root, XmlNamespaceManager nsmgr)
448         {
449             var xPrefix = nsmgr.LookupPrefix(XamlParser.X2006Uri) ?? nsmgr.LookupPrefix(XamlParser.X2009Uri);
450             if (xPrefix == null)
451                 yield break;
452
453             XmlNodeList names =
454                 root.SelectNodes(
455                     "//*[@" + xPrefix + ":Name" +
456                     "][not(ancestor:: __f__:DataTemplate) and not(ancestor:: __f__:ControlTemplate) and not(ancestor:: __f__:Style) and not(ancestor:: __f__:VisualStateManager.VisualStateGroups)]", nsmgr);
457             foreach (XmlNode node in names) {
458                 var name = GetAttributeValue(node, "Name", XamlParser.X2006Uri, XamlParser.X2009Uri);
459                 var typeArguments = GetAttributeValue(node, "TypeArguments", XamlParser.X2006Uri, XamlParser.X2009Uri);
460                 var fieldModifier = GetAttributeValue(node, "FieldModifier", XamlParser.X2006Uri, XamlParser.X2009Uri);
461
462                 var xmlType = new XmlType(node.NamespaceURI, node.LocalName,
463                                           typeArguments != null
464                                           ? TypeArgumentsParser.ParseExpression(typeArguments, nsmgr, null)
465                                           : null);
466
467                 var access = MemberAttributes.Public;
468                 if (fieldModifier != null) {
469                     switch (fieldModifier.ToLowerInvariant()) {
470                         default:
471                         case "private":
472                             access = MemberAttributes.Private;
473                             break;
474                         case "public":
475                             access = MemberAttributes.Public;
476                             break;
477                         case "protected":
478                             access = MemberAttributes.Family;
479                             break;
480                         case "internal":
481                         case "notpublic": //WPF syntax
482                             access = MemberAttributes.Assembly;
483                             break;
484                     }
485                 }
486
487                 yield return new CodeMemberField {
488                     Name = name,
489                     Type = GetType(xmlType, node.GetNamespaceOfPrefix),
490                     Attributes = access,
491                     CustomAttributes = { GeneratedCodeAttrDecl }
492                 };
493             }
494         }
495
496         static bool GetXamlCompilationProcessingInstruction(XmlDocument xmlDoc)
497         {
498             var instruction = xmlDoc.SelectSingleNode("processing-instruction('xaml-comp')") as XmlProcessingInstruction;
499             if (instruction == null)
500                 return false;
501
502             var parts = instruction.Data.Split(' ', '=');
503             string compileValue = null;
504             var indexOfCompile = Array.IndexOf(parts, "compile");
505             if (indexOfCompile != -1)
506                 compileValue = parts[indexOfCompile + 1].Trim('"', '\'');
507             return compileValue.Equals("true", StringComparison.InvariantCultureIgnoreCase);
508         }
509
510         static CodeTypeReference GetType(XmlType xmlType,
511             Func<string, string> getNamespaceOfPrefix = null)
512         {
513             var type = xmlType.Name;
514             var ns = GetClrNamespace(xmlType.NamespaceUri, xmlType.Name);
515             if (ns != null)
516                 type = $"{ns}.{type}";
517
518             if (xmlType.TypeArguments != null)
519                 type = $"{type}`{xmlType.TypeArguments.Count}";
520
521             var returnType = new CodeTypeReference(type);
522             if (ns != null)
523                 returnType.Options |= CodeTypeReferenceOptions.GlobalReference;
524
525             if (xmlType.TypeArguments != null)
526                 foreach (var typeArg in xmlType.TypeArguments)
527                     returnType.TypeArguments.Add(GetType(typeArg, getNamespaceOfPrefix));
528
529             return returnType;
530         }
531
532         static string GetClrNamespace(string namespaceuri, string className)
533         {
534             XmlnsInfo xmlnsInfo = null;
535
536             xmlnsNameToInfo.TryGetValue(namespaceuri, out xmlnsInfo);
537
538             if (null != xmlnsInfo)
539             {
540                 string nameSpace = xmlnsInfo.GetNameSpace(className);
541
542                 if (null != nameSpace)
543                 {
544                     return nameSpace;
545                 }
546             }
547
548             if (namespaceuri == "http://tizen.org/Tizen.NUI/2018/XAML")
549                 return "Tizen.NUI.Xaml";
550             if (namespaceuri == XamlParser.XFUri)
551                 return "Tizen.NUI.Xaml";
552             if (namespaceuri == XamlParser.X2009Uri)
553                 return "System";
554             //if (namespaceuri != XamlParser.X2006Uri && !namespaceuri.StartsWith("clr-namespace", StringComparison.InvariantCulture) && !namespaceuri.StartsWith("using", StringComparison.InvariantCulture))
555             //    throw new Exception($"Can't load types from xmlns {namespaceuri}");
556             return XmlnsHelper.ParseNamespaceFromXmlns(namespaceuri);
557         }
558
559         static string GetAttributeValue(XmlNode node, string localName, params string[] namespaceURIs)
560         {
561             if (node == null)
562                 throw new ArgumentNullException(nameof(node));
563             if (localName == null)
564                 throw new ArgumentNullException(nameof(localName));
565             if (namespaceURIs == null)
566                 throw new ArgumentNullException(nameof(namespaceURIs));
567             foreach (var namespaceURI in namespaceURIs) {
568                 var attr = node.Attributes[localName, namespaceURI];
569                 if (attr == null)
570                     continue;
571                 return attr.Value;
572             }
573             return null;
574         }
575     }
576 }
577