PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/Sources/Tools/ResourceWrapper.Generator/ResourceParser.cs

#
C# | 368 lines | 319 code | 23 blank | 26 comment | 116 complexity | bca38780e62289508e18bb135cf667c3 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Text.RegularExpressions;
  6. using System.Xml;
  7. namespace ResourceWrapper.Generator {
  8. /// <summary>
  9. /// Parses and creates list of Items for generator to produce code.
  10. /// Expect the following syntax on comment:
  11. /// - minus in the first position of the comment turn off any parsing and property will be generated.
  12. /// !(value1, value2, ... valueN) list of allowed items. The value of the resource is expected to be one of the value1-valueN
  13. /// If there is no formating parameters than comment is ignored
  14. /// If there are formating parameters comment should declare parameters of formating function: {type1 parameter1, type2 parameter2, ... typeM parameterM}
  15. /// </summary>
  16. internal class ResourceParser {
  17. public static IEnumerable<ResourceItem> Parse(string file, bool enforceParameterDeclaration, IEnumerable<string> satelites, out int errors, out int warnings) {
  18. XmlDocument resource = new XmlDocument();
  19. resource.Load(file);
  20. XmlNodeList nodeList = ResourceParser.SelectResources(resource);
  21. if(nodeList != null && 0 < nodeList.Count) {
  22. ResourceParser parser = new ResourceParser(file, enforceParameterDeclaration, satelites);
  23. List<ResourceItem> list = new List<ResourceItem>();
  24. Action<ResourceItem> assign = item => { if(item != null) { list.Add(item); } };
  25. parser.Parse(nodeList,
  26. (string name, string value, string comment) => assign(parser.GenerateInclude(name, value, comment)),
  27. (string name, string value, string comment) => assign(parser.GenerateString(name, value, comment))
  28. );
  29. if(parser.errorCount == 0 && parser.satelites != null) {
  30. parser.VerifySatelites(list);
  31. }
  32. errors = parser.errorCount;
  33. warnings = parser.warningCount;
  34. if(parser.errorCount == 0) {
  35. return list;
  36. } else {
  37. return null;
  38. }
  39. }
  40. errors = 0;
  41. warnings = 0;
  42. return Enumerable.Empty<ResourceItem>();
  43. }
  44. public static IEnumerable<ResourceItem> Parse(string file, bool enforceParameterDeclaration, IEnumerable<string> satelites) {
  45. int errors;
  46. int warnings;
  47. return ResourceParser.Parse(file, enforceParameterDeclaration, satelites, out errors, out warnings);
  48. }
  49. private static XmlNodeList SelectResources(XmlDocument resource) {
  50. return resource.SelectNodes("/root/data");
  51. }
  52. private string fileName;
  53. private readonly bool enforceParameterDeclaration;
  54. private readonly IEnumerable<string> satelites;
  55. private const RegexOptions regexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline;
  56. private readonly Regex variantList = new Regex(@"^!\((?<list>.*)\)", regexOptions);
  57. // {int index, string message} hello, world {System.Int32 param} comment {} {MyType value1, Other value2, OneMore last}
  58. private readonly Regex functionParameters = new Regex(@"\{(?<param>[^}]+)\}", regexOptions);
  59. // a.b.c.d a, int i, string text, System.Int32 index
  60. private readonly Regex parameterDeclaration = new Regex(@"^(?<type>[A-Za-z_][A-Za-z_0-9]*(\s*\.\s*[A-Za-z_][A-Za-z_0-9]*)*)\s+(?<name>[A-Za-z_][A-Za-z_0-9]*)$", regexOptions);
  61. private int errorCount;
  62. private int warningCount;
  63. private ResourceParser(string file, bool enforceParameterDeclaration, IEnumerable<string> satelites) {
  64. this.fileName = file;
  65. this.enforceParameterDeclaration = enforceParameterDeclaration;
  66. this.satelites = satelites;
  67. this.errorCount = 0;
  68. this.warningCount = 0;
  69. }
  70. private void Parse(XmlNodeList nodeList, Action<string, string, string> generateInclude, Action<string, string, string> generateString) {
  71. foreach(XmlNode node in nodeList) {
  72. XmlAttribute nodeName = node.Attributes["name"];
  73. if(nodeName == null) {
  74. this.Error("Unknown Node", "Resource name is missing");
  75. continue;
  76. }
  77. string name = nodeName.InnerText.Trim();
  78. XmlNode nodeValue = node.SelectSingleNode("value");
  79. if(nodeValue == null) {
  80. this.Error(name, "Value missing");
  81. continue;
  82. }
  83. string value = nodeValue.InnerText.Trim();
  84. XmlNode nodeComment = node.SelectSingleNode("comment");
  85. string comment = (nodeComment != null) ? nodeComment.InnerText.Trim() : string.Empty;
  86. if(node.Attributes["type"] != null) {
  87. generateInclude(name, value, comment);
  88. } else {
  89. generateString(name, value, comment);
  90. }
  91. }
  92. }
  93. private void VerifySatelites(List<ResourceItem> itemList) {
  94. Dictionary<string, ResourceItem> items = new Dictionary<string,ResourceItem>(itemList.Count);
  95. itemList.ForEach(i => items.Add(i.Name, i));
  96. string mainFile = this.fileName;
  97. Action<string> unknownResource = name => this.Warning(name, "resource does not exist in the main resource file \"{0}\"", mainFile);
  98. foreach(string file in this.satelites) {
  99. XmlDocument resource = new XmlDocument();
  100. resource.Load(file);
  101. XmlNodeList nodeList = ResourceParser.SelectResources(resource);
  102. if(nodeList != null && 0 < nodeList.Count) {
  103. this.fileName = file;
  104. this.Parse(nodeList,
  105. (string name, string value, string comment) => {
  106. ResourceItem item;
  107. if(items.TryGetValue(name, out item)) {
  108. ResourceItem satellite = this.GenerateInclude(name, value, comment);
  109. if(item.Type != satellite.Type) {
  110. this.Error(name, "type of file resource is different in main resource file \"{0}\" and language resource file \"{1}\"", mainFile, file);
  111. }
  112. } else {
  113. unknownResource(name);
  114. }
  115. },
  116. (string name, string value, string comment) => {
  117. ResourceItem item;
  118. if(items.TryGetValue(name, out item)) {
  119. if(!item.SuppressValidation) {
  120. int count = this.ValidateFormatItems(name, value, false);
  121. if(count != (item.Parameters == null ? 0 : item.Parameters.Count)) {
  122. this.Warning(name, "number of parameters is different from the same resource in the main resource file \"{0}\"", mainFile);
  123. } else if(item.LocalizationVariants != null) {
  124. if(!item.LocalizationVariants.Contains(value)) {
  125. this.Error(name, "provided value is not in variant list defined in main resource file: \"{0}\"", mainFile);
  126. }
  127. }
  128. }
  129. } else {
  130. unknownResource(name);
  131. }
  132. }
  133. );
  134. }
  135. }
  136. this.fileName = mainFile;
  137. }
  138. private bool Error(string nodeName, string errorText, params object[] args) {
  139. //"C:\Projects\TestApp\TestApp\Subfolder\TextMessage.resx(10,1): error URW001: nodeName: my error"
  140. Message.Error("{0}(1,1): error URW001: {1}: {2}", this.fileName, nodeName, this.Format(errorText, args));
  141. this.errorCount++;
  142. return false;
  143. }
  144. private void Warning(string nodeName, string errorText, params object[] args) {
  145. Message.Warning("{0}(1,1): warning: {1}: {2}", this.fileName, nodeName, this.Format(errorText, args));
  146. this.warningCount++;
  147. Message.Flush();
  148. }
  149. private bool Corrupted(string nodeName) {
  150. return this.Error(nodeName, "Structure of the value node is corrupted");
  151. }
  152. private string Format(string format, params object[] args) {
  153. return string.Format(CultureInfo.InvariantCulture, format, args);
  154. }
  155. private ResourceItem GenerateInclude(string name, string value, string comment) {
  156. string[] list = value.Split(';');
  157. if(list.Length < 2) {
  158. this.Corrupted(name);
  159. return null;
  160. }
  161. string file = list[0];
  162. list = list[1].Split(',');
  163. if(list.Length < 2) {
  164. this.Corrupted(name);
  165. return null;
  166. }
  167. string type = list[0].Trim();
  168. if(0 == this.errorCount) {
  169. if(type == "System.String") {
  170. file = this.Format("content of the file: \"{0}\"", file);
  171. }
  172. return new ResourceItem(name, file, type);
  173. }
  174. return null;
  175. }
  176. private ResourceItem GenerateString(string name, string value, string comment) {
  177. ResourceItem item = new ResourceItem(name, value, "string");
  178. if(!comment.StartsWith("-")) {
  179. if(!this.IsVariantList(item, value, comment)) {
  180. this.ParseFormatParameters(item, value, comment);
  181. }
  182. } else {
  183. item.SuppressValidation = true;
  184. }
  185. return (0 == this.errorCount) ? item : null;
  186. }
  187. private bool IsVariantList(ResourceItem item, string value, string comment) {
  188. Match match = this.variantList.Match(comment);
  189. if(match.Success) {
  190. string listText = match.Groups["list"].Value;
  191. string[] variants = listText.Split(',');
  192. List<string> list = new List<string>();
  193. foreach(string var in variants) {
  194. string text = var.Trim();
  195. if(0 < text.Length) {
  196. list.Add(text);
  197. }
  198. }
  199. item.LocalizationVariants = list;
  200. if(!list.Contains(value)) {
  201. this.Error(item.Name, "Localization variants does not contain provided value: {0}", value);
  202. }
  203. }
  204. return match.Success;
  205. }
  206. private void ParseFormatParameters(ResourceItem item, string value, string comment) {
  207. int count = this.ValidateFormatItems(item.Name, value, true);
  208. if(0 < count) {
  209. Match paramsMatch = this.functionParameters.Match(comment);
  210. if(paramsMatch.Success) {
  211. string[] list = paramsMatch.Groups["param"].Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
  212. List<Parameter> parameterList = new List<Parameter>(list.Length);
  213. foreach(string text in list) {
  214. if(!string.IsNullOrWhiteSpace(text)) {
  215. Match parameterMatch = this.parameterDeclaration.Match(text.Trim());
  216. if(parameterMatch.Success) {
  217. parameterList.Add(new Parameter(parameterMatch.Groups["type"].Value, parameterMatch.Groups["name"].Value));
  218. } else {
  219. this.Error(item.Name, "bad parameter declaration: {0}", text.Trim());
  220. }
  221. }
  222. }
  223. if(parameterList.Count != count) {
  224. this.Error(item.Name, "number of parameters expected by value of the string do not match to provided parameter list in comment");
  225. }
  226. item.Parameters = parameterList;
  227. } else {
  228. string error = "string value contains formating placeholders, but function parameters declaration is missing in comment.";
  229. if(this.enforceParameterDeclaration) {
  230. this.Error(item.Name, error);
  231. } else {
  232. this.Warning(item.Name, error);
  233. }
  234. }
  235. }
  236. }
  237. /// <summary>
  238. /// Validates format string in a manner very close to what string.Format do.
  239. /// </summary>
  240. /// <param name="name"></param>
  241. /// <param name="value"></param>
  242. /// <param name="requareAllParameters"></param>
  243. /// <returns></returns>
  244. private int ValidateFormatItems(string name, string value, bool requareAllParameters) {
  245. Func<int> error = () => {
  246. this.Error(name, "Invalid formating item.");
  247. return -1;
  248. };
  249. HashSet<int> indexes = new HashSet<int>();
  250. for(int i = 0; i < value.Length; i++) {
  251. if('}' == value[i]) {
  252. i++;
  253. if(!(i < value.Length && '}' == value[i])) {
  254. this.Error(name, "Input string is not in correct format");
  255. return -1;
  256. }
  257. } else if('{' == value[i]) {
  258. i++;
  259. if(i < value.Length && '{' == value[i]) {
  260. continue; // skip escaped {
  261. }
  262. // Formating item is started
  263. // First is parameter number. Spaces are not allowed in front or it.
  264. bool isNumber = false;
  265. int index = 0;
  266. while(i < value.Length && '0' <= value[i] && value[i] <= '9' && index < 1000000) {
  267. index = index * 10 + value[i] - '0';
  268. isNumber = true;
  269. i++;
  270. }
  271. if(!isNumber || 1000000 <= index) {
  272. return error();
  273. }
  274. indexes.Add(index);
  275. //Skip spaces
  276. while(i < value.Length && ' ' == value[i]) {
  277. i++;
  278. }
  279. //Check for alignment
  280. if(i < value.Length && ',' == value[i]) {
  281. i++;
  282. while(i < value.Length && ' ' == value[i]) {
  283. i++;
  284. }
  285. if(i < value.Length && '-' == value[i]) {
  286. i++; //skip sign
  287. }
  288. isNumber = false;
  289. index = 0;
  290. while(i < value.Length && '0' <= value[i] && value[i] <= '9' && index < 1000000) {
  291. index = index * 10 + value[i] - '0';
  292. isNumber = true;
  293. i++;
  294. }
  295. if(!isNumber || 1000000 <= index) {
  296. return error();
  297. }
  298. }
  299. //Skip spaces
  300. while(i < value.Length && ' ' == value[i]) {
  301. i++;
  302. }
  303. //Check for format string.
  304. if(i < value.Length && ':' == value[i]) {
  305. // Inside format string. It is allowed to have escaped open and closed braces, so skip them until single }
  306. for(;;) {
  307. i++;
  308. while(i < value.Length && '}' != value[i]) {
  309. if('{' == value[i]) {
  310. i++;
  311. if(!(i < value.Length && '{' == value[i])) {
  312. return error();
  313. }
  314. }
  315. i++;
  316. }
  317. if(i + 1 < value.Length && '}' == value[i + 1]) {
  318. i++;
  319. } else {
  320. break;
  321. }
  322. }
  323. }
  324. if(!(i < value.Length && '}' == value[i])) {
  325. return error();
  326. }
  327. }
  328. }
  329. // Check that all format parameters are present.
  330. int current = 0;
  331. foreach(int index in indexes.OrderBy(i => i)) {
  332. if(index != current++) {
  333. if(requareAllParameters) {
  334. this.Error(name, "parameter number {0} is missing in the string", current - 1);
  335. } else {
  336. this.Warning(name, "parameter number {0} is missing in the string", current - 1);
  337. break;
  338. }
  339. return -1; // report just one missing parameter number
  340. }
  341. }
  342. return indexes.Count;
  343. }
  344. }
  345. }