@joebigelow / wix / commits / e04caab1

Separate "format" from "convert"

Closes wixtoolset/issues#6215

Rob Mensching committed Jul 18, 2020 at 14:57 UTC e04caab11fb8f2cac4d575ef1e352221bd421586
8 files changed +589 -428
src/WixToolset.Converters/ConvertCommand.cs
+22 -247
@@ -3,283 +3,58 @@
3 namespace WixToolset.Converters
4 {
5 using System;
6 - using System.Collections.Generic;
7 - using System.IO;
6 using System.Threading;
7 using System.Threading.Tasks;
10 - using System.Xml;
11 - using WixToolset.Extensibility.Data;
8 using WixToolset.Extensibility.Services;
9
14 - internal class ConvertCommand : ICommandLineCommand
10 + internal class ConvertCommand : FixupCommandBase
11 {
16 - private const string SettingsFileDefault = "wixcop.settings.xml";
12 + private const string SettingsFileDefault = "wix.convert.settings.xml";
13
14 public ConvertCommand(IWixToolsetServiceProvider serviceProvider)
15 {
16 this.Messaging = serviceProvider.GetService<IMessaging>();
21 -
22 - this.IndentationAmount = 4; // default indentation amount
23 - this.ErrorsAsWarnings = new HashSet<string>();
24 - this.ExemptFiles = new HashSet<string>();
25 - this.IgnoreErrors = new HashSet<string>();
26 - this.SearchPatternResults = new HashSet<string>();
27 - this.SearchPatterns = new List<string>();
17 }
18
19 private IMessaging Messaging { get; }
20
32 - public bool ShowLogo { get; private set; }
33 -
34 - public bool StopParsing { get; private set; }
35 -
36 - private bool ShowHelp { get; set; }
37 -
38 - private HashSet<string> ErrorsAsWarnings { get; }
39 -
40 - private HashSet<string> ExemptFiles { get; }
41 -
42 - private bool FixErrors { get; set; }
43 -
44 - private int IndentationAmount { get; set; }
45 -
46 - private HashSet<string> IgnoreErrors { get; }
47 -
48 - private HashSet<string> SearchPatternResults { get; }
49 -
50 - private List<string> SearchPatterns { get; }
51 -
52 - private string SettingsFile1 { get; set; }
53 -
54 - private string SettingsFile2 { get; set; }
55 -
56 - private bool SubDirectories { get; set; }
57 -
58 - public bool TryParseArgument(ICommandLineParser parser, string argument)
59 - {
60 - if (!parser.IsSwitch(argument))
61 - {
62 - this.SearchPatterns.Add(argument);
63 - return true;
64 - }
65 -
66 - var parameter = argument.Substring(1);
67 - switch (parameter.ToLowerInvariant())
68 - {
69 - case "?":
70 - this.ShowHelp = true;
71 - this.ShowLogo = true;
72 - this.StopParsing = true;
73 - return true;
74 -
75 - case "f":
76 - this.FixErrors = true;
77 - return true;
78 -
79 - case "nologo":
80 - this.ShowLogo = false;
81 - return true;
82 -
83 - case "s":
84 - this.SubDirectories = true;
85 - return true;
86 -
87 - default: // other parameters
88 - if (parameter.StartsWith("set1", StringComparison.Ordinal))
89 - {
90 - this.SettingsFile1 = parameter.Substring(4);
91 - return true;
92 - }
93 - else if (parameter.StartsWith("set2", StringComparison.Ordinal))
94 - {
95 - this.SettingsFile2 = parameter.Substring(4);
96 - return true;
97 - }
98 - else if (parameter.StartsWith("indent:", StringComparison.Ordinal))
99 - {
100 - try
101 - {
102 - this.IndentationAmount = Convert.ToInt32(parameter.Substring(7));
103 - }
104 - catch
105 - {
106 - parser.ErrorArgument = parameter; // $"Invalid numeric argument: {parameter}";
107 - }
108 - return true;
109 - }
110 -
111 - return false;
112 - }
113 - }
114 -
115 - public Task<int> ExecuteAsync(CancellationToken cancellationToken)
21 + public override Task<int> ExecuteAsync(CancellationToken cancellationToken)
22 {
23 if (this.ShowHelp)
24 {
25 DisplayHelp();
120 - return Task.FromResult(1);
121 - }
122 -
123 - // parse the settings if any were specified
124 - if (null != this.SettingsFile1 || null != this.SettingsFile2)
125 - {
126 - this.ParseSettingsFiles(this.SettingsFile1, this.SettingsFile2);
127 - }
128 - else
129 - {
130 - if (File.Exists(ConvertCommand.SettingsFileDefault))
131 - {
132 - this.ParseSettingsFiles(ConvertCommand.SettingsFileDefault, null);
133 - }
26 + return Task.FromResult(-1);
27 }
28
29 var converter = new WixConverter(this.Messaging, this.IndentationAmount, this.ErrorsAsWarnings, this.IgnoreErrors);
30
138 - var errors = this.InspectSubDirectories(converter, Path.GetFullPath("."), cancellationToken);
31 + this.ParseSettings(SettingsFileDefault);
32 +
33 + var errors = base.Inspect(Inspector, cancellationToken);
34
140 - foreach (var searchPattern in this.SearchPatterns)
35 + return Task.FromResult(errors);
36 +
37 + int Inspector(string file, bool fix)
38 {
142 - if (!this.SearchPatternResults.Contains(searchPattern))
143 - {
144 - Console.Error.WriteLine("Could not find file \"{0}\"", searchPattern);
145 - errors++;
146 - }
39 + return converter.ConvertFile(file, fix);
40 }
148 -
149 - return Task.FromResult(errors != 0 ? 2 : 0);
41 }
42
43 private static void DisplayHelp()
44 {
154 - Console.WriteLine(" usage: wix.exe convert sourceFile [sourceFile ...]");
45 Console.WriteLine();
156 - Console.WriteLine(" -f fix errors automatically for writable files");
157 - Console.WriteLine(" -nologo suppress displaying the logo information");
158 - Console.WriteLine(" -s search for matching files in current dir and subdirs");
159 - Console.WriteLine(" -set1<file> primary settings file");
160 - Console.WriteLine(" -set2<file> secondary settings file (overrides primary)");
161 - Console.WriteLine(" -indent:<n> indentation multiple (overrides default of 4)");
162 - Console.WriteLine(" -? this help information");
46 + Console.WriteLine("Usage: wix convert [options] sourceFile [sourceFile ...]");
47 Console.WriteLine();
164 - Console.WriteLine(" sourceFile may use wildcards like *.wxs");
165 - }
166 -
167 - /// <summary>
168 - /// Get the files that match a search path pattern.
169 - /// </summary>
170 - /// <param name="baseDir">The base directory at which to begin the search.</param>
171 - /// <param name="searchPath">The search path pattern.</param>
172 - /// <returns>The files matching the pattern.</returns>
173 - private static string[] GetFiles(string baseDir, string searchPath)
174 - {
175 - // convert alternate directory separators to the standard one
176 - var filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
177 - var lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar);
178 - string[] files = null;
179 -
180 - try
181 - {
182 - if (0 > lastSeparator)
183 - {
184 - files = Directory.GetFiles(baseDir, filePath);
185 - }
186 - else // found directory separator
187 - {
188 - var searchPattern = filePath.Substring(lastSeparator + 1);
189 -
190 - files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), searchPattern);
191 - }
192 - }
193 - catch (DirectoryNotFoundException)
194 - {
195 - // don't let this function throw the DirectoryNotFoundException. (this exception
196 - // occurs for non-existant directories and invalid characters in the searchPattern)
197 - }
198 -
199 - return files;
200 - }
201 -
202 - /// <summary>
203 - /// Inspect sub-directories.
204 - /// </summary>
205 - /// <param name="directory">The directory whose sub-directories will be inspected.</param>
206 - /// <returns>The number of errors that were found.</returns>
207 - private int InspectSubDirectories(WixConverter converter, string directory, CancellationToken cancellationToken)
208 - {
209 - var errors = 0;
210 -
211 - foreach (var searchPattern in this.SearchPatterns)
212 - {
213 - foreach (var sourceFilePath in GetFiles(directory, searchPattern))
214 - {
215 - cancellationToken.ThrowIfCancellationRequested();
216 -
217 - var file = new FileInfo(sourceFilePath);
218 -
219 - if (!this.ExemptFiles.Contains(file.Name.ToUpperInvariant()))
220 - {
221 - this.SearchPatternResults.Add(searchPattern);
222 - errors += converter.ConvertFile(file.FullName, this.FixErrors);
223 - }
224 - }
225 - }
226 -
227 - if (this.SubDirectories)
228 - {
229 - foreach (var childDirectoryPath in Directory.GetDirectories(directory))
230 - {
231 - errors += this.InspectSubDirectories(converter, childDirectoryPath, cancellationToken);
232 - }
233 - }
234 -
235 - return errors;
236 - }
237 -
238 - /// <summary>
239 - /// Parse the primary and secondary settings files.
240 - /// </summary>
241 - /// <param name="localSettingsFile1">The primary settings file.</param>
242 - /// <param name="localSettingsFile2">The secondary settings file.</param>
243 - private void ParseSettingsFiles(string localSettingsFile1, string localSettingsFile2)
244 - {
245 - if (null == localSettingsFile1 && null != localSettingsFile2)
246 - {
247 - throw new ArgumentException("Cannot specify a secondary settings file (set2) without a primary settings file (set1).", nameof(localSettingsFile2));
248 - }
249 -
250 - var settingsFile = localSettingsFile1;
251 - while (null != settingsFile)
252 - {
253 - var doc = new XmlDocument();
254 - doc.Load(settingsFile);
255 -
256 - // get the types of tests that will have their errors displayed as warnings
257 - var testsIgnoredElements = doc.SelectNodes("/Settings/IgnoreErrors/Test");
258 - foreach (XmlElement test in testsIgnoredElements)
259 - {
260 - var key = test.GetAttribute("Id");
261 - this.IgnoreErrors.Add(key);
262 - }
263 -
264 - // get the types of tests that will have their errors displayed as warnings
265 - var testsAsWarningsElements = doc.SelectNodes("/Settings/ErrorsAsWarnings/Test");
266 - foreach (XmlElement test in testsAsWarningsElements)
267 - {
268 - var key = test.GetAttribute("Id");
269 - this.ErrorsAsWarnings.Add(key);
270 - }
271 -
272 - // get the exempt files
273 - var localExemptFiles = doc.SelectNodes("/Settings/ExemptFiles/File");
274 - foreach (XmlElement file in localExemptFiles)
275 - {
276 - var key = file.GetAttribute("Name").ToUpperInvariant();
277 - this.ExemptFiles.Add(key);
278 - }
279 -
280 - settingsFile = localSettingsFile2;
281 - localSettingsFile2 = null;
282 - }
48 + Console.WriteLine("Options:");
49 + Console.WriteLine(" -h|--help Show command line help.");
50 + Console.WriteLine(" --nologo Suppress displaying the logo information.");
51 + Console.WriteLine(" -n|--dry-run Only display errors, do not update files.");
52 + Console.WriteLine(" -r|--recurse Search for matching files in current dir and subdirs.");
53 + Console.WriteLine(" -set1<file> Primary settings file.");
54 + Console.WriteLine(" -set2<file> Secondary settings file (overrides primary).");
55 + Console.WriteLine(" -indent:<n> Indentation multiple (overrides default of 4).");
56 + Console.WriteLine();
57 + Console.WriteLine(" sourceFile may use wildcards like *.wxs");
58 }
59 }
60 }
src/WixToolset.Converters/ConverterExtensionCommandLine.cs
+9 -2
@@ -21,8 +21,11 @@ namespace WixToolset.Converters
21
22 private IWixToolsetServiceProvider ServiceProvider { get; }
23
24 - // TODO: Do something with CommandLineSwitches
25 - public override IEnumerable<ExtensionCommandLineSwitch> CommandLineSwitches => base.CommandLineSwitches;
24 + public override IEnumerable<ExtensionCommandLineSwitch> CommandLineSwitches => new ExtensionCommandLineSwitch[]
25 + {
26 + new ExtensionCommandLineSwitch { Switch = "convert", Description = "Convert v3 source code to v4 source code." },
27 + new ExtensionCommandLineSwitch { Switch = "format", Description = "Ensures consistent formatting of source code." },
28 + };
29
30 public override bool TryParseCommand(ICommandLineParser parser, string argument, out ICommandLineCommand command)
31 {
@@ -32,6 +35,10 @@ namespace WixToolset.Converters
35 {
36 command = new ConvertCommand(this.ServiceProvider);
37 }
38 + else if ("format".Equals(argument, StringComparison.OrdinalIgnoreCase))
39 + {
40 + command = new FormatCommand(this.ServiceProvider);
41 + }
42
43 return command != null;
44 }
src/WixToolset.Converters/FixupCommandBase.cs new
+267
@@ -0,0 +1,267 @@
1 +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2 +
3 +namespace WixToolset.Converters
4 +{
5 + using System;
6 + using System.Collections.Generic;
7 + using System.IO;
8 + using System.Threading;
9 + using System.Threading.Tasks;
10 + using System.Xml;
11 + using WixToolset.Extensibility.Data;
12 + using WixToolset.Extensibility.Services;
13 +
14 + internal abstract class FixupCommandBase : ICommandLineCommand
15 + {
16 + protected FixupCommandBase()
17 + {
18 + this.IndentationAmount = 4; // default indentation amount
19 + this.ErrorsAsWarnings = new HashSet<string>();
20 + this.ExemptFiles = new HashSet<string>();
21 + this.IgnoreErrors = new HashSet<string>();
22 + this.SearchPatternResults = new HashSet<string>();
23 + this.SearchPatterns = new List<string>();
24 + }
25 +
26 + public bool ShowLogo { get; private set; }
27 +
28 + public bool StopParsing { get; private set; }
29 +
30 + protected bool ShowHelp { get; set; }
31 +
32 + protected bool DryRun { get; set; }
33 +
34 + protected HashSet<string> ErrorsAsWarnings { get; }
35 +
36 + protected HashSet<string> IgnoreErrors { get; }
37 +
38 + protected HashSet<string> ExemptFiles { get; }
39 +
40 + protected int IndentationAmount { get; set; }
41 +
42 + protected bool Recurse { get; set; }
43 +
44 + private HashSet<string> SearchPatternResults { get; }
45 +
46 + private List<string> SearchPatterns { get; }
47 +
48 + private string SettingsFile1 { get; set; }
49 +
50 + private string SettingsFile2 { get; set; }
51 +
52 + public bool TryParseArgument(ICommandLineParser parser, string argument)
53 + {
54 + if (!parser.IsSwitch(argument))
55 + {
56 + this.SearchPatterns.Add(argument);
57 + return true;
58 + }
59 +
60 + var parameter = argument.Substring(1);
61 + switch (parameter.ToLowerInvariant())
62 + {
63 + case "?":
64 + case "h":
65 + case "-help":
66 + this.ShowHelp = true;
67 + this.ShowLogo = true;
68 + this.StopParsing = true;
69 + return true;
70 +
71 + case "n":
72 + case "--dry-run":
73 + this.DryRun = true;
74 + return true;
75 +
76 + case "nologo":
77 + case "-nologo":
78 + this.ShowLogo = false;
79 + return true;
80 +
81 + case "s":
82 + case "r":
83 + case "-recurse":
84 + this.Recurse = true;
85 + return true;
86 +
87 + default: // other parameters
88 + if (parameter.StartsWith("set1", StringComparison.Ordinal))
89 + {
90 + this.SettingsFile1 = parameter.Substring(4);
91 + return true;
92 + }
93 + else if (parameter.StartsWith("set2", StringComparison.Ordinal))
94 + {
95 + this.SettingsFile2 = parameter.Substring(4);
96 + return true;
97 + }
98 + else if (parameter.StartsWith("indent:", StringComparison.Ordinal))
99 + {
100 + try
101 + {
102 + this.IndentationAmount = Convert.ToInt32(parameter.Substring(7));
103 + }
104 + catch
105 + {
106 + parser.ErrorArgument = parameter; // $"Invalid numeric argument: {parameter}";
107 + }
108 + return true;
109 + }
110 +
111 + return false;
112 + }
113 + }
114 +
115 + public abstract Task<int> ExecuteAsync(CancellationToken cancellationToken);
116 +
117 + protected void ParseSettings(string defaultSettingsFile)
118 + {
119 + // parse the settings if any were specified
120 + if (null != this.SettingsFile1 || null != this.SettingsFile2)
121 + {
122 + this.ParseSettingsFiles(this.SettingsFile1, this.SettingsFile2);
123 + }
124 + else
125 + {
126 + if (File.Exists(defaultSettingsFile))
127 + {
128 + this.ParseSettingsFiles(defaultSettingsFile, null);
129 + }
130 + }
131 + }
132 +
133 + protected int Inspect(Func<string, bool, int> inspector, CancellationToken cancellationToken)
134 + {
135 + var errors = this.InspectSubDirectories(inspector, Path.GetFullPath("."), cancellationToken);
136 +
137 + foreach (var searchPattern in this.SearchPatterns)
138 + {
139 + if (!this.SearchPatternResults.Contains(searchPattern))
140 + {
141 + Console.Error.WriteLine("Could not find file \"{0}\"", searchPattern);
142 + errors++;
143 + }
144 + }
145 +
146 + return errors;
147 + }
148 +
149 + /// <summary>
150 + /// Inspect sub-directories.
151 + /// </summary>
152 + /// <param name="directory">The directory whose sub-directories will be inspected.</param>
153 + /// <returns>The number of errors that were found.</returns>
154 + private int InspectSubDirectories(Func<string, bool, int> inspector, string directory, CancellationToken cancellationToken)
155 + {
156 + var errors = 0;
157 +
158 + foreach (var searchPattern in this.SearchPatterns)
159 + {
160 + foreach (var sourceFilePath in GetFiles(directory, searchPattern))
161 + {
162 + cancellationToken.ThrowIfCancellationRequested();
163 +
164 + var file = new FileInfo(sourceFilePath);
165 +
166 + if (!this.ExemptFiles.Contains(file.Name.ToUpperInvariant()))
167 + {
168 + this.SearchPatternResults.Add(searchPattern);
169 + errors += inspector(file.FullName, !this.DryRun);
170 + }
171 + }
172 + }
173 +
174 + if (this.Recurse)
175 + {
176 + foreach (var childDirectoryPath in Directory.GetDirectories(directory))
177 + {
178 + errors += this.InspectSubDirectories(inspector, childDirectoryPath, cancellationToken);
179 + }
180 + }
181 +
182 + return errors;
183 + }
184 +
185 + /// <summary>
186 + /// Parse the primary and secondary settings files.
187 + /// </summary>
188 + /// <param name="localSettingsFile1">The primary settings file.</param>
189 + /// <param name="localSettingsFile2">The secondary settings file.</param>
190 + private void ParseSettingsFiles(string localSettingsFile1, string localSettingsFile2)
191 + {
192 + if (null == localSettingsFile1 && null != localSettingsFile2)
193 + {
194 + throw new ArgumentException("Cannot specify a secondary settings file (set2) without a primary settings file (set1).", nameof(localSettingsFile2));
195 + }
196 +
197 + var settingsFile = localSettingsFile1;
198 + while (null != settingsFile)
199 + {
200 + var doc = new XmlDocument();
201 + doc.Load(settingsFile);
202 +
203 + // get the types of tests that will have their errors displayed as warnings
204 + var testsIgnoredElements = doc.SelectNodes("/Settings/IgnoreErrors/Test");
205 + foreach (XmlElement test in testsIgnoredElements)
206 + {
207 + var key = test.GetAttribute("Id");
208 + this.IgnoreErrors.Add(key);
209 + }
210 +
211 + // get the types of tests that will have their errors displayed as warnings
212 + var testsAsWarningsElements = doc.SelectNodes("/Settings/ErrorsAsWarnings/Test");
213 + foreach (XmlElement test in testsAsWarningsElements)
214 + {
215 + var key = test.GetAttribute("Id");
216 + this.ErrorsAsWarnings.Add(key);
217 + }
218 +
219 + // get the exempt files
220 + var localExemptFiles = doc.SelectNodes("/Settings/ExemptFiles/File");
221 + foreach (XmlElement file in localExemptFiles)
222 + {
223 + var key = file.GetAttribute("Name").ToUpperInvariant();
224 + this.ExemptFiles.Add(key);
225 + }
226 +
227 + settingsFile = localSettingsFile2;
228 + localSettingsFile2 = null;
229 + }
230 + }
231 +
232 + /// <summary>
233 + /// Get the files that match a search path pattern.
234 + /// </summary>
235 + /// <param name="baseDir">The base directory at which to begin the search.</param>
236 + /// <param name="searchPath">The search path pattern.</param>
237 + /// <returns>The files matching the pattern.</returns>
238 + private static string[] GetFiles(string baseDir, string searchPath)
239 + {
240 + // convert alternate directory separators to the standard one
241 + var filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
242 + var lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar);
243 + string[] files = null;
244 +
245 + try
246 + {
247 + if (0 > lastSeparator)
248 + {
249 + files = Directory.GetFiles(baseDir, filePath);
250 + }
251 + else // found directory separator
252 + {
253 + var searchPattern = filePath.Substring(lastSeparator + 1);
254 +
255 + files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), searchPattern);
256 + }
257 + }
258 + catch (DirectoryNotFoundException)
259 + {
260 + // don't let this function throw the DirectoryNotFoundException. (this exception
261 + // occurs for non-existant directories and invalid characters in the searchPattern)
262 + }
263 +
264 + return files;
265 + }
266 + }
267 +}
src/WixToolset.Converters/FormatCommand.cs new
+60
@@ -0,0 +1,60 @@
1 +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2 +
3 +namespace WixToolset.Converters
4 +{
5 + using System;
6 + using System.Threading;
7 + using System.Threading.Tasks;
8 + using WixToolset.Extensibility.Services;
9 +
10 + internal class FormatCommand : FixupCommandBase
11 + {
12 + private const string SettingsFileDefault = "wix.format.settings.xml";
13 +
14 + public FormatCommand(IWixToolsetServiceProvider serviceProvider)
15 + {
16 + this.Messaging = serviceProvider.GetService<IMessaging>();
17 + }
18 +
19 + private IMessaging Messaging { get; }
20 +
21 + public override Task<int> ExecuteAsync(CancellationToken cancellationToken)
22 + {
23 + if (this.ShowHelp)
24 + {
25 + DisplayHelp();
26 + return Task.FromResult(-1);
27 + }
28 +
29 + var converter = new WixConverter(this.Messaging, this.IndentationAmount, this.ErrorsAsWarnings, this.IgnoreErrors);
30 +
31 + this.ParseSettings(SettingsFileDefault);
32 +
33 + var errors = base.Inspect(Inspector, cancellationToken);
34 +
35 + return Task.FromResult(errors);
36 +
37 + int Inspector(string file, bool fix)
38 + {
39 + return converter.FormatFile(file, fix);
40 + }
41 + }
42 +
43 + private static void DisplayHelp()
44 + {
45 + Console.WriteLine();
46 + Console.WriteLine("Usage: wix format [options] sourceFile [sourceFile ...]");
47 + Console.WriteLine();
48 + Console.WriteLine("Options:");
49 + Console.WriteLine(" -h|--help Show command line help.");
50 + Console.WriteLine(" --nologo Suppress displaying the logo information.");
51 + Console.WriteLine(" -n|--dry-run Only display errors, do not update files.");
52 + Console.WriteLine(" -r|--recurse Search for matching files in current dir and subdirs.");
53 + Console.WriteLine(" -set1<file> Primary settings file.");
54 + Console.WriteLine(" -set2<file> Secondary settings file (overrides primary).");
55 + Console.WriteLine(" -indent:<n> Indentation multiple (overrides default of 4).");
56 + Console.WriteLine();
57 + Console.WriteLine(" sourceFile may use wildcards like *.wxs");
58 + }
59 + }
60 +}
src/WixToolset.Converters/WixConverter.cs
+109 -70
@@ -19,6 +19,12 @@ namespace WixToolset.Converters
19 /// </summary>
20 public class WixConverter
21 {
22 + private enum ConvertOperation
23 + {
24 + Convert,
25 + Format,
26 + }
27 +
28 private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]", RegexOptions.Compiled);
29 private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}", RegexOptions.Compiled); // non 'words' and assorted valid characters
30
@@ -107,18 +113,6 @@ namespace WixToolset.Converters
113 { "http://schemas.microsoft.com/wix/2006/WixUnit", "http://wixtoolset.org/schemas/v4/wixunit" },
114 };
115
110 - private readonly static SortedSet<string> Wix3Namespaces = new SortedSet<string>
111 - {
112 - "http://schemas.microsoft.com/wix/2006/wi",
113 - "http://schemas.microsoft.com/wix/2006/localization",
114 - };
115 -
116 - private readonly static SortedSet<string> Wix4Namespaces = new SortedSet<string>
117 - {
118 - "http://wixtoolset.org/schemas/v4/wxs",
119 - "http://wixtoolset.org/schemas/v4/wxl",
120 - };
121 -
116 private readonly Dictionary<XName, Action<XElement>> ConvertElementMapping;
117
118 /// <summary>
@@ -193,6 +187,8 @@ namespace WixToolset.Converters
187
188 private int IndentationAmount { get; set; }
189
190 + private ConvertOperation Operation { get; set; }
191 +
192 private string SourceFile { get; set; }
193
194 private int SourceVersion { get; set; }
@@ -205,22 +201,11 @@ namespace WixToolset.Converters
201 /// <returns>The number of errors found.</returns>
202 public int ConvertFile(string sourceFile, bool saveConvertedFile)
203 {
208 - XDocument document;
209 -
210 - // Set the instance info.
211 - this.Errors = 0;
212 - this.SourceFile = sourceFile;
213 - this.SourceVersion = 0;
204 + var document = this.OpenSourceFile(sourceFile);
205
215 - try
206 + if (document is null)
207 {
217 - document = XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
218 - }
219 - catch (XmlException e)
220 - {
221 - this.OnError(ConverterTestType.XmlException, null, "The xml is invalid. Detail: '{0}'", e.Message);
222 -
223 - return this.Errors;
208 + return 1;
209 }
210
211 this.ConvertDocument(document);
@@ -228,17 +213,7 @@ namespace WixToolset.Converters
213 // Fix errors if requested and necessary.
214 if (saveConvertedFile && 0 < this.Errors)
215 {
231 - try
232 - {
233 - using (var writer = XmlWriter.Create(this.SourceFile, new XmlWriterSettings { OmitXmlDeclaration = true }))
234 - {
235 - document.Save(writer);
236 - }
237 - }
238 - catch (UnauthorizedAccessException)
239 - {
240 - this.OnError(ConverterTestType.UnauthorizedAccessException, null, "Could not write to file.");
241 - }
216 + this.SaveDocument(document);
217 }
218
219 return this.Errors;
@@ -251,13 +226,68 @@ namespace WixToolset.Converters
226 /// <returns>The number of errors found.</returns>
227 public int ConvertDocument(XDocument document)
228 {
229 + // Reset the instance info.
230 this.Errors = 0;
231 this.SourceVersion = 0;
232 + this.Operation = ConvertOperation.Convert;
233 +
234 + // Remove the declaration.
235 + if (null != document.Declaration)
236 + {
237 + if (this.OnError(ConverterTestType.DeclarationPresent, null, "This file contains an XML declaration on the first line."))
238 + {
239 + document.Declaration = null;
240 + }
241 + }
242 +
243 + TrimLeadingText(document);
244 +
245 + // Start converting the nodes at the top.
246 + this.ConvertNodes(document.Nodes(), 0);
247 +
248 + return this.Errors;
249 + }
250
257 - var declaration = document.Declaration;
251 + /// <summary>
252 + /// Format a file.
253 + /// </summary>
254 + /// <param name="sourceFile">The file to format.</param>
255 + /// <param name="saveConvertedFile">Option to save the format errors that are found.</param>
256 + /// <returns>The number of errors found.</returns>
257 + public int FormatFile(string sourceFile, bool saveConvertedFile)
258 + {
259 + var document = this.OpenSourceFile(sourceFile);
260 +
261 + if (document is null)
262 + {
263 + return 1;
264 + }
265 +
266 + this.FormatDocument(document);
267 +
268 + // Fix errors if requested and necessary.
269 + if (saveConvertedFile && 0 < this.Errors)
270 + {
271 + this.SaveDocument(document);
272 + }
273 +
274 + return this.Errors;
275 + }
276 +
277 + /// <summary>
278 + /// Format a document.
279 + /// </summary>
280 + /// <param name="document">The document to format.</param>
281 + /// <returns>The number of errors found.</returns>
282 + public int FormatDocument(XDocument document)
283 + {
284 + // Reset the instance info.
285 + this.Errors = 0;
286 + this.SourceVersion = 0;
287 + this.Operation = ConvertOperation.Format;
288
289 // Remove the declaration.
260 - if (null != declaration)
290 + if (null != document.Declaration)
291 {
292 if (this.OnError(ConverterTestType.DeclarationPresent, null, "This file contains an XML declaration on the first line."))
293 {
@@ -273,6 +303,37 @@ namespace WixToolset.Converters
303 return this.Errors;
304 }
305
306 + private XDocument OpenSourceFile(string sourceFile)
307 + {
308 + this.SourceFile = sourceFile;
309 +
310 + try
311 + {
312 + return XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
313 + }
314 + catch (XmlException e)
315 + {
316 + this.OnError(ConverterTestType.XmlException, null, "The xml is invalid. Detail: '{0}'", e.Message);
317 + }
318 +
319 + return null;
320 + }
321 +
322 + private void SaveDocument(XDocument document)
323 + {
324 + try
325 + {
326 + using (var writer = XmlWriter.Create(this.SourceFile, new XmlWriterSettings { OmitXmlDeclaration = true }))
327 + {
328 + document.Save(writer);
329 + }
330 + }
331 + catch (UnauthorizedAccessException)
332 + {
333 + this.OnError(ConverterTestType.UnauthorizedAccessException, null, "Could not write to file.");
334 + }
335 + }
336 +
337 private void ConvertNodes(IEnumerable<XNode> nodes, int level)
338 {
339 // Note we operate on a copy of the node list since we may
@@ -901,7 +962,10 @@ namespace WixToolset.Converters
962 /// <returns>Returns true indicating that action should be taken on this error, and false if it should be ignored.</returns>
963 private bool OnError(ConverterTestType converterTestType, XObject node, string message, params object[] args)
964 {
904 - if (this.IgnoreErrors.Contains(converterTestType)) // ignore the error
965 + // Ignore the error if explicitly ignored or outside the range of the current operation.
966 + if (this.IgnoreErrors.Contains(converterTestType) ||
967 + (this.Operation == ConvertOperation.Convert && converterTestType < ConverterTestType.DeclarationPresent) ||
968 + (this.Operation == ConvertOperation.Format && converterTestType > ConverterTestType.DeclarationPresent))
969 {
970 return false;
971 }
@@ -909,7 +973,7 @@ namespace WixToolset.Converters
973 // Increase the error count.
974 this.Errors++;
975
912 - var sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber);
976 + var sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wix.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber);
977 var warning = this.ErrorsAsWarnings.Contains(converterTestType);
978 var display = String.Format(CultureInfo.CurrentCulture, message, args);
979
@@ -1049,40 +1113,20 @@ namespace WixToolset.Converters
1113 /// </summary>
1114 UnauthorizedAccessException,
1115
1052 - /// <summary>
1053 - /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'.
1054 - /// </summary>
1055 - DeclarationEncodingWrong,
1056 -
1057 - /// <summary>
1058 - /// Displayed when the XML declaration is missing from the source file.
1059 - /// </summary>
1060 - DeclarationMissing,
1061 -
1062 - /// <summary>
1063 - /// Displayed when the whitespace preceding a CDATA node is wrong.
1064 - /// </summary>
1065 - WhitespacePrecedingCDATAWrong,
1066 -
1116 /// <summary>
1117 /// Displayed when the whitespace preceding a node is wrong.
1118 /// </summary>
1119 WhitespacePrecedingNodeWrong,
1120
1121 /// <summary>
1073 - /// Displayed when an element is not empty as it should be.
1074 - /// </summary>
1075 - NotEmptyElement,
1076 -
1077 - /// <summary>
1078 - /// Displayed when the whitespace following a CDATA node is wrong.
1122 + /// Displayed when the whitespace preceding an end element is wrong.
1123 /// </summary>
1080 - WhitespaceFollowingCDATAWrong,
1124 + WhitespacePrecedingEndElementWrong,
1125
1126 /// <summary>
1083 - /// Displayed when the whitespace preceding an end element is wrong.
1127 + /// Displayed when the XML declaration is present in the source file.
1128 /// </summary>
1085 - WhitespacePrecedingEndElementWrong,
1129 + DeclarationPresent,
1130
1131 /// <summary>
1132 /// Displayed when the xmlns attribute is missing from the document element.
@@ -1154,11 +1198,6 @@ namespace WixToolset.Converters
1198 /// </summary>
1199 AutoGuidUnnecessary,
1200
1157 - /// <summary>
1158 - /// Displayed when the XML declaration is present in the source file.
1159 - /// </summary>
1160 - DeclarationPresent,
1161 -
1201 /// <summary>
1202 /// The Feature Absent attribute renamed to AllowAbsent.
1203 /// </summary>
src/test/WixToolsetTest.Converters/ConverterFixture.cs
-104
@@ -39,110 +39,6 @@ namespace WixToolsetTest.Converters
39 Assert.Equal(expected, actual);
40 }
41
42 - [Fact]
43 - public void CanFixWhitespace()
44 - {
45 - var parse = String.Join(Environment.NewLine,
46 - "<?xml version='1.0' encoding='utf-8'?>",
47 - "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
48 - " <Fragment>",
49 - " <Property Id='Prop'",
50 - " Value='Val'>",
51 - " </Property>",
52 - " </Fragment>",
53 - "</Wix>");
54 -
55 - var expected = String.Join(Environment.NewLine,
56 - "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
57 - " <Fragment>",
58 - " <Property Id=\"Prop\" Value=\"Val\" />",
59 - " </Fragment>",
60 - "</Wix>");
61 -
62 - var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
63 -
64 - var messaging = new MockMessaging();
65 - var converter = new WixConverter(messaging, 4, null, null);
66 -
67 - var errors = converter.ConvertDocument(document);
68 -
69 - var actual = UnformattedDocumentString(document);
70 -
71 - Assert.Equal(expected, actual);
72 - Assert.Equal(5, errors);
73 - }
74 -
75 - [Fact]
76 - public void CanPreserveNewLines()
77 - {
78 - var parse = String.Join(Environment.NewLine,
79 - "<?xml version='1.0' encoding='utf-8'?>",
80 - "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
81 - " <Fragment>",
82 - "",
83 - " <Property Id='Prop' Value='Val' />",
84 - "",
85 - " </Fragment>",
86 - "</Wix>");
87 -
88 - var expected = String.Join(Environment.NewLine,
89 - "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
90 - " <Fragment>",
91 - "",
92 - " <Property Id=\"Prop\" Value=\"Val\" />",
93 - "",
94 - " </Fragment>",
95 - "</Wix>");
96 -
97 - var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
98 -
99 - var messaging = new MockMessaging();
100 - var converter = new WixConverter(messaging, 4, null, null);
101 -
102 - var conversions = converter.ConvertDocument(document);
103 -
104 - var actual = UnformattedDocumentString(document);
105 -
106 - Assert.Equal(expected, actual);
107 - Assert.Equal(4, conversions);
108 - }
109 -
110 - [Fact]
111 - public void CanConvertWithNewLineAtEndOfFile()
112 - {
113 - var parse = String.Join(Environment.NewLine,
114 - "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
115 - " <Fragment>",
116 - "",
117 - " <Property Id='Prop' Value='Val' />",
118 - "",
119 - " </Fragment>",
120 - "</Wix>",
121 - "");
122 -
123 - var expected = String.Join(Environment.NewLine,
124 - "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
125 - " <Fragment>",
126 - "",
127 - " <Property Id=\"Prop\" Value=\"Val\" />",
128 - "",
129 - " </Fragment>",
130 - "</Wix>",
131 - "");
132 -
133 - var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
134 -
135 - var messaging = new MockMessaging();
136 - var converter = new WixConverter(messaging, 4, null, null);
137 -
138 - var conversions = converter.ConvertDocument(document);
139 -
140 - var actual = UnformattedDocumentString(document);
141 -
142 - Assert.Equal(expected, actual);
143 - Assert.Equal(3, conversions);
144 - }
145 -
42 [Fact]
43 public void CanConvertMainNamespace()
44 {
src/test/WixToolsetTest.Converters/ConverterIntegrationFixture.cs
+5 -5
@@ -84,7 +84,7 @@ namespace WixToolsetTest.Converters
84 var settingsFile = Path.Combine(folder, "wixcop.settings.xml");
85
86 var result = RunConversion(targetFile, settingsFile: settingsFile);
87 - Assert.Equal(2, result.ExitCode);
87 + Assert.Equal(7, result.ExitCode);
88
89 var expected = File.ReadAllText(Path.Combine(folder, afterFileName)).Replace("\r\n", "\n");
90 var actual = File.ReadAllText(targetFile).Replace("\r\n", "\n");
@@ -108,7 +108,7 @@ namespace WixToolsetTest.Converters
108 File.Copy(Path.Combine(folder, beforeFileName), Path.Combine(baseFolder, beforeFileName));
109
110 var result = RunConversion(targetFile);
111 - Assert.Equal(2, result.ExitCode);
111 + Assert.Equal(10, result.ExitCode);
112
113 var expected = File.ReadAllText(Path.Combine(folder, afterFileName)).Replace("\r\n", "\n");
114 var actual = File.ReadAllText(targetFile).Replace("\r\n", "\n");
@@ -133,7 +133,7 @@ namespace WixToolsetTest.Converters
133
134 var result = RunConversion(targetFile);
135
136 - Assert.Equal(2, result.ExitCode);
136 + Assert.Equal(10, result.ExitCode);
137 Assert.Single(result.Messages.Where(message => message.ToString().EndsWith("(QtExecCmdTimeoutAmbiguous)")));
138
139 var expected = File.ReadAllText(Path.Combine(folder, afterFileName)).Replace("\r\n", "\n");
@@ -142,7 +142,7 @@ namespace WixToolsetTest.Converters
142
143 // still fails because QtExecCmdTimeoutAmbiguous is unfixable
144 var result2 = RunConversion(targetFile);
145 - Assert.Equal(2, result2.ExitCode);
145 + Assert.Equal(1, result2.ExitCode);
146 }
147 }
148
@@ -153,7 +153,7 @@ namespace WixToolsetTest.Converters
153 var exitCode = WixRunner.Execute(new[]
154 {
155 "convert",
156 - fixErrors ? "-f" : null,
156 + fixErrors ? null : "--dry-run",
157 String.IsNullOrEmpty(settingsFile) ? null : "-set1" + settingsFile,
158 targetFile
159 }, serviceProvider, out var messages);
src/test/WixToolsetTest.Converters/FormatFixture.cs new
+117
@@ -0,0 +1,117 @@
1 +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2 +
3 +namespace WixToolsetTest.Converters
4 +{
5 + using System;
6 + using System.Xml.Linq;
7 + using WixToolset.Converters;
8 + using WixToolsetTest.Converters.Mocks;
9 + using Xunit;
10 +
11 + public class FormatFixture : BaseConverterFixture
12 + {
13 + [Fact]
14 + public void CanFixWhitespace()
15 + {
16 + var parse = String.Join(Environment.NewLine,
17 + "<?xml version='1.0' encoding='utf-8'?>",
18 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
19 + " <Fragment>",
20 + " <Property Id='Prop'",
21 + " Value='Val'>",
22 + " </Property>",
23 + " </Fragment>",
24 + "</Wix>");
25 +
26 + var expected = String.Join(Environment.NewLine,
27 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
28 + " <Fragment>",
29 + " <Property Id=\"Prop\" Value=\"Val\" />",
30 + " </Fragment>",
31 + "</Wix>");
32 +
33 + var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
34 +
35 + var messaging = new MockMessaging();
36 + var converter = new WixConverter(messaging, 4, null, null);
37 +
38 + var errors = converter.FormatDocument(document);
39 +
40 + var actual = UnformattedDocumentString(document);
41 +
42 + Assert.Equal(expected, actual);
43 + Assert.Equal(5, errors);
44 + }
45 +
46 + [Fact]
47 + public void CanPreserveNewLines()
48 + {
49 + var parse = String.Join(Environment.NewLine,
50 + "<?xml version='1.0' encoding='utf-8'?>",
51 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
52 + " <Fragment>",
53 + "",
54 + " <Property Id='Prop' Value='Val' />",
55 + "",
56 + " </Fragment>",
57 + "</Wix>");
58 +
59 + var expected = String.Join(Environment.NewLine,
60 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
61 + " <Fragment>",
62 + "",
63 + " <Property Id=\"Prop\" Value=\"Val\" />",
64 + "",
65 + " </Fragment>",
66 + "</Wix>");
67 +
68 + var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
69 +
70 + var messaging = new MockMessaging();
71 + var converter = new WixConverter(messaging, 4, null, null);
72 +
73 + var conversions = converter.FormatDocument(document);
74 +
75 + var actual = UnformattedDocumentString(document);
76 +
77 + Assert.Equal(expected, actual);
78 + Assert.Equal(4, conversions);
79 + }
80 +
81 + [Fact]
82 + public void CanFormatWithNewLineAtEndOfFile()
83 + {
84 + var parse = String.Join(Environment.NewLine,
85 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
86 + " <Fragment>",
87 + "",
88 + " <Property Id='Prop' Value='Val' />",
89 + "",
90 + " </Fragment>",
91 + "</Wix>",
92 + "");
93 +
94 + var expected = String.Join(Environment.NewLine,
95 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
96 + " <Fragment>",
97 + "",
98 + " <Property Id=\"Prop\" Value=\"Val\" />",
99 + "",
100 + " </Fragment>",
101 + "</Wix>",
102 + "");
103 +
104 + var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
105 +
106 + var messaging = new MockMessaging();
107 + var converter = new WixConverter(messaging, 4, null, null);
108 +
109 + var conversions = converter.FormatDocument(document);
110 +
111 + var actual = UnformattedDocumentString(document);
112 +
113 + Assert.Equal(expected, actual);
114 + Assert.Equal(3, conversions);
115 + }
116 + }
117 +}