From a32b857d9672672e5b31725695665c1e4466ec2a Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Sun, 14 Jan 2018 14:35:13 +0000 Subject: [PATCH] When compiling Razor files, generate namespace based on directory path --- .../Cli/Commands/BuildRazorCommand.cs | 1 + .../Core/RazorCompilation/RazorCompiler.cs | 78 +++++++++++++++---- .../RazorCompilerTest.cs | 56 +++++++++---- 3 files changed, 107 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.Blazor.Build/Cli/Commands/BuildRazorCommand.cs b/src/Microsoft.Blazor.Build/Cli/Commands/BuildRazorCommand.cs index 0a9280bac2..f9acd45ec8 100644 --- a/src/Microsoft.Blazor.Build/Cli/Commands/BuildRazorCommand.cs +++ b/src/Microsoft.Blazor.Build/Cli/Commands/BuildRazorCommand.cs @@ -47,6 +47,7 @@ namespace Microsoft.Blazor.Build.Cli.Commands using (var outputWriter = new StreamWriter(outputFilePath.Value())) { var diagnostics = new RazorCompiler().CompileFiles( + sourceDirPathValue, inputRazorFilePaths, "Blazor", // TODO: Add required option for namespace outputWriter, diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs index d11b9c58b2..6d59fdab34 100644 --- a/src/Microsoft.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs +++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs @@ -21,42 +21,73 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation /// /// Writes C# source code representing Blazor components defined by Razor files. /// - /// Paths to the input files. - /// The namespace for the generated classes. + /// Path to a directory containing input files. + /// Paths to the input files relative to . The generated namespaces will be based on these relative paths. + /// The base namespace for the generated classes. /// A to which C# source code will be written. /// If not null, additional information will be written to this . /// A collection of instances representing any warnings or errors that were encountered. - public ICollection CompileFiles(IEnumerable inputPaths, string outputNamespace, TextWriter resultOutput, TextWriter verboseOutput) + public ICollection CompileFiles( + string inputRootPath, + IEnumerable inputPaths, + string baseNamespace, + TextWriter resultOutput, + TextWriter verboseOutput) => inputPaths.SelectMany(path => { using (var reader = File.OpenText(path)) { - return CompileSingleFile(path, reader, outputNamespace, resultOutput, verboseOutput); + return CompileSingleFile(inputRootPath, path, reader, baseNamespace, resultOutput, verboseOutput); } }).ToList(); /// /// Writes C# source code representing a Blazor component defined by a Razor file. /// - /// The path to the input file. - /// The namespace for the generated class. + /// Path to a directory containing input files. + /// Paths to the input files relative to . The generated namespaces will be based on these relative paths. + /// The base namespace for the generated class. /// A to which C# source code will be written. /// If not null, additional information will be written to this . /// An enumerable of instances representing any warnings or errors that were encountered. - public IEnumerable CompileSingleFile(string inputFilePath, TextReader inputFileReader, string outputNamespace, TextWriter resultOutput, TextWriter verboseOutput) + public IEnumerable CompileSingleFile( + string inputRootPath, + string inputFilePath, + TextReader inputFileReader, + string baseNamespace, + TextWriter resultOutput, + TextWriter verboseOutput) { + if (inputFileReader == null) + { + throw new ArgumentNullException(nameof(inputFileReader)); + } + if (resultOutput == null) { throw new ArgumentNullException(nameof(resultOutput)); } + if (string.IsNullOrEmpty(inputRootPath)) + { + throw new ArgumentException("Cannot be null or empty.", nameof(inputRootPath)); + } + + if (string.IsNullOrEmpty(baseNamespace)) + { + throw new ArgumentException("Cannot be null or empty.", nameof(baseNamespace)); + } + try { verboseOutput?.WriteLine($"Compiling {inputFilePath}..."); - var className = GetClassName(inputFilePath); + var (itemNamespace, itemClassName) = GetNamespaceAndClassName(inputRootPath, inputFilePath); + var combinedNamespace = string.IsNullOrEmpty(itemNamespace) + ? baseNamespace + : $"{baseNamespace}.{itemNamespace}"; - resultOutput.WriteLine($"namespace {outputNamespace} {{"); - resultOutput.WriteLine($"public class {className}"); + resultOutput.WriteLine($"namespace {combinedNamespace} {{"); + resultOutput.WriteLine($"public class {itemClassName}"); resultOutput.WriteLine("{"); resultOutput.WriteLine("}"); resultOutput.WriteLine("}"); @@ -82,15 +113,32 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation } } - private static string GetClassName(string inputFilePath) + private static (string, string) GetNamespaceAndClassName(string inputRootPath, string inputFilePath) { - var basename = Path.GetFileNameWithoutExtension(inputFilePath); - if (!ClassNameRegex.IsMatch(basename)) + // First represent inputFilePath as a path relative to inputRootPath. Not using Path.GetRelativePath + // because it doesn't handle cases like inputFilePath="\\something.cs". + var inputFilePathAbsolute = Path.GetFullPath(Path.Combine(inputRootPath, inputFilePath)); + var inputRootPathWithTrailingSeparator = inputRootPath.EndsWith(Path.DirectorySeparatorChar) + ? inputRootPath + : (inputRootPath + Path.DirectorySeparatorChar); + var inputFilePathRelative = inputFilePathAbsolute.StartsWith(inputRootPathWithTrailingSeparator) + ? inputFilePathAbsolute.Substring(inputRootPathWithTrailingSeparator.Length) + : throw new RazorCompilerException($"File is not within source root directory: '{inputFilePath}'"); + + // Use the set of directory names in the relative path as namespace + var inputDirname = Path.GetDirectoryName(inputFilePathRelative); + var resultNamespace = inputDirname + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(" ", string.Empty); + + // Use the filename as class name + var inputBasename = Path.GetFileNameWithoutExtension(inputFilePathRelative); + if (!ClassNameRegex.IsMatch(inputBasename)) { - throw new RazorCompilerException($"Invalid name '{basename}'. The name must be valid for a C# class name."); + throw new RazorCompilerException($"Invalid name '{inputBasename}'. The name must be valid for a C# class name."); } - return basename; + return (resultNamespace, inputBasename); } } } diff --git a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs index f8985451b7..9e50e3fdd3 100644 --- a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs @@ -21,7 +21,8 @@ namespace Microsoft.Blazor.Build.Test { // Arrange/Act var result = CompileToCSharp( - "x:\\dir\\subdir\\Filename with spaces.cshtml", + "x:\\dir\\subdir", + "Filename with spaces.cshtml", "ignored code", "ignored namespace"); @@ -34,28 +35,55 @@ namespace Microsoft.Blazor.Build.Test }); } - [Fact] - public void CreatesClassWithCorrectNameAndNamespace() + [Theory] + [InlineData("\\unrelated.cs")] + [InlineData("..\\outsideroot.cs")] + public void RejectsFilenameOutsideRoot(string filename) { // Arrange/Act + var result = CompileToCSharp( + "x:\\dir\\subdir", + filename, + "ignored code", + "ignored namespace"); + + // Assert + Assert.Collection(result.Diagnostics, + item => + { + Assert.Equal(RazorCompilerDiagnostic.DiagnosticType.Error, item.Type); + Assert.StartsWith($"File is not within source root directory: '{filename}'", item.Message); + }); + } + + [Theory] + [InlineData("ItemAtRoot.cs", "Test.Base", "ItemAtRoot")] + [InlineData(".\\ItemAtRoot.cs", "Test.Base", "ItemAtRoot")] + [InlineData("x:\\dir\\subdir\\ItemAtRoot.cs", "Test.Base", "ItemAtRoot")] + [InlineData("Dir1\\MyFile.cs", "Test.Base.Dir1", "MyFile")] + [InlineData("Dir1\\Dir2\\MyFile.cs", "Test.Base.Dir1.Dir2", "MyFile")] + public void CreatesClassWithCorrectNameAndNamespace(string relativePath, string expectedNamespace, string expectedClassName) + { + // Arrange/Acts var result = CompileToAssembly( - "x:\\dir\\subdir\\Filename.cshtml", + "x:\\dir\\subdir", + relativePath, "{* No code *}", - "MyCompany.MyNamespace"); + "Test.Base"); // Assert Assert.Empty(result.Diagnostics); Assert.Collection(result.Assembly.GetTypes(), type => { - Assert.Equal("Filename", type.Name); - Assert.Equal("MyCompany.MyNamespace", type.Namespace); + Assert.Equal(expectedNamespace, type.Namespace); + Assert.Equal(expectedClassName, type.Name); }); } - private static CompileToAssemblyResult CompileToAssembly(string cshtmlFilename, string cshtmlContent, string outputNamespace) + private static CompileToAssemblyResult CompileToAssembly(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent, string outputNamespace) { - var csharpResult = CompileToCSharp(cshtmlFilename, cshtmlContent, outputNamespace); + var csharpResult = CompileToCSharp(cshtmlRootPath, cshtmlRelativePath, cshtmlContent, outputNamespace); if (csharpResult.Diagnostics.Any()) { var diagnosticsLog = string.Join(Environment.NewLine, @@ -82,16 +110,17 @@ namespace Microsoft.Blazor.Build.Test { compilation.Emit(peStream); + var diagnostics = compilation.GetDiagnostics(); return new CompileToAssemblyResult { - Diagnostics = compilation.GetDiagnostics(), + Diagnostics = diagnostics, VerboseLog = csharpResult.VerboseLog, - Assembly = Assembly.Load(peStream.ToArray()) + Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray()) }; } } - private static CompileToCSharpResult CompileToCSharp(string cshtmlFilename, string cshtmlContent, string outputNamespace) + private static CompileToCSharpResult CompileToCSharp(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent, string outputNamespace) { using (var resultStream = new MemoryStream()) using (var resultWriter = new StreamWriter(resultStream)) @@ -100,7 +129,8 @@ namespace Microsoft.Blazor.Build.Test using (var inputReader = new StringReader(cshtmlContent)) { var diagnostics = new RazorCompiler().CompileSingleFile( - cshtmlFilename, + cshtmlRootPath, + cshtmlRelativePath, inputReader, outputNamespace, resultWriter,