From 61a1fbdd5b65ef2779ba14d05f3ba40accd9274d Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Thu, 15 Oct 2020 10:22:35 -0700 Subject: [PATCH 1/5] Quote docs output path in qsc.rsp. --- src/QuantumSdk/Sdk/Sdk.targets | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/QuantumSdk/Sdk/Sdk.targets b/src/QuantumSdk/Sdk/Sdk.targets index defcb6dafd..242f27bf0d 100644 --- a/src/QuantumSdk/Sdk/Sdk.targets +++ b/src/QuantumSdk/Sdk/Sdk.targets @@ -71,8 +71,8 @@ - <_QscDocsPackageeId Condition="'$(PackageId)' != ''">$(PackageId) - <_QscDocsPackageeId Condition="'$(QsharpDocsPackageId)' != ''">$(QsharpDocsPackageId) + <_QscDocsPackageId Condition="'$(PackageId)' != ''">$(PackageId) + <_QscDocsPackageId Condition="'$(QsharpDocsPackageId)' != ''">$(QsharpDocsPackageId) <_QscCommandIsExecutableFlag Condition="'$(ResolvedQsharpOutputType)' == 'QsharpExe'">--build-exe <_QscCommandOutputFlag>--output "$(GeneratedFilesOutputPath)" <_QscCommandInputFlag Condition="@(QsharpCompile->Count()) > 0">--input "@(QsharpCompile,'" "')" @@ -85,8 +85,8 @@ <_QscCommandPredefinedAssemblyProperties Condition="'$(DefaultSimulator)' != ''">$(_QscCommandPredefinedAssemblyProperties) DefaultSimulator:$(DefaultSimulator) <_QscCommandPredefinedAssemblyProperties Condition="'$(ExecutionTarget)' != ''">$(_QscCommandPredefinedAssemblyProperties) ExecutionTarget:$(ExecutionTarget) <_QscCommandPredefinedAssemblyProperties Condition="'$(ExposeReferencesViaTestNames)'">$(_QscCommandPredefinedAssemblyProperties) ExposeReferencesViaTestNames:true - <_QscCommandPredefinedAssemblyProperties Condition="'$(_QscDocsPackageeId)' != ''">$(_QscCommandPredefinedAssemblyProperties) DocsPackageId:$(_QscDocsPackageeId) - <_QscCommandPredefinedAssemblyProperties Condition="'$(QsharpDocsGeneration)'">$(_QscCommandPredefinedAssemblyProperties) DocsOutputPath:$(QsharpDocsOutputPath) + <_QscCommandPredefinedAssemblyProperties Condition="'$(_QscDocsPackageId)' != ''">$(_QscCommandPredefinedAssemblyProperties) DocsPackageId:$(_QscDocsPackageeId) + <_QscCommandPredefinedAssemblyProperties Condition="'$(QsharpDocsGeneration)'">$(_QscCommandPredefinedAssemblyProperties) DocsOutputPath:"$(QsharpDocsOutputPath)" <_QscCommandAssemblyPropertiesFlag>--assembly-properties $(_QscCommandPredefinedAssemblyProperties) $(QscCommandAssemblyProperties) <_QscPackageLoadFallbackFoldersFlag Condition="@(ResolvedPackageLoadFallbackFolders->Count()) > 0">--package-load-fallback-folders "@(ResolvedPackageLoadFallbackFolders,'" "')" <_QscCommandArgs>--proj "$(PathCompatibleAssemblyName)" $(_QscCommandIsExecutableFlag) $(_QscCommandInputFlag) $(_QscCommandOutputFlag) $(_QscCommandReferencesFlag) $(_QscCommandLoadFlag) $(_QscCommandRuntimeFlag) $(_QscCommandTargetDecompositionsFlag) $(_QscPackageLoadFallbackFoldersFlag) $(_QscCommandTestNamesFlag) $(_QscCommandAssemblyPropertiesFlag) $(AdditionalQscArguments) From cdfc66095c03ffa6ec76fa4dca9947b13e5d995f Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Thu, 15 Oct 2020 10:23:09 -0700 Subject: [PATCH 2/5] Add better diagnostics to DocumentationWriter. --- .../DocumentationGeneration.cs | 14 ++- .../DocumentationWriter.cs | 96 ++++++++++++++----- 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/Documentation/DocumentationGenerator/DocumentationGeneration.cs b/src/Documentation/DocumentationGenerator/DocumentationGeneration.cs index 536e156242..cfdcf70944 100644 --- a/src/Documentation/DocumentationGenerator/DocumentationGeneration.cs +++ b/src/Documentation/DocumentationGenerator/DocumentationGeneration.cs @@ -70,14 +70,24 @@ public bool PreconditionVerification(QsCompilation compilation) public bool Transformation(QsCompilation compilation, out QsCompilation transformed) { - transformed = new ProcessDocComments( + var docProcessor = new ProcessDocComments( this.AssemblyConstants.TryGetValue("DocsOutputPath", out var path) ? path : null, this.AssemblyConstants.TryGetValue("DocsPackageId", out var packageName) ? packageName : null - ).OnCompilation(compilation); + ); + + if (docProcessor.Writer != null) + { + docProcessor.Writer.OnDiagnostic += diagnostic => + { + this.diagnostics.Add(diagnostic); + }; + } + + transformed = docProcessor.OnCompilation(compilation); return true; } diff --git a/src/Documentation/DocumentationGenerator/DocumentationWriter.cs b/src/Documentation/DocumentationGenerator/DocumentationWriter.cs index ed6edb2ab5..4bba29230f 100644 --- a/src/Documentation/DocumentationGenerator/DocumentationWriter.cs +++ b/src/Documentation/DocumentationGenerator/DocumentationWriter.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading.Tasks; using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Quantum.QsCompiler; using Microsoft.Quantum.QsCompiler.Documentation; using Microsoft.Quantum.QsCompiler.SyntaxTree; @@ -18,6 +20,12 @@ namespace Microsoft.Quantum.Documentation /// public class DocumentationWriter { + /// + /// An event that is raised on diagnostics about documentation + /// writing (e.g., if an I/O problem prevents writing to disk). + /// + public event Action? OnDiagnostic; + /// /// Path to which output documentation files should be written. /// @@ -34,6 +42,36 @@ public class DocumentationWriter private readonly string PackageLink; + private async Task TryWithExceptionsAsDiagnostics(string description, Func action, DiagnosticSeverity severity = DiagnosticSeverity.Warning) + { + try + { + await action(); + } + catch (Exception ex) + { + this.OnDiagnostic?.Invoke(new IRewriteStep.Diagnostic + { + Severity = severity, + Message = $"Exception raised when {description}:\n{ex.Message}", + Range = null, + Source = null, + Stage = IRewriteStep.Stage.Transformation, + }); + } + } + + private async Task WriteAllTextAsync(string filename, string contents) + { + await this.TryWithExceptionsAsDiagnostics( + $"writing output to {filename}", + async () => await File.WriteAllTextAsync( + Path.Join(this.OutputPath, $"{filename.ToLowerInvariant()}.md"), + contents + ) + ); + } + /// /// Initializes a new instance of the /// class. @@ -52,14 +90,28 @@ public DocumentationWriter(string outputPath, string? packageName) this.packageName = packageName; // If the output path is not null, make sure the directory exists. - if (outputPath != null && !Directory.Exists(outputPath)) + if (outputPath != null) { - Directory.CreateDirectory(outputPath); + this.OnDiagnostic?.Invoke(new IRewriteStep.Diagnostic + { + Severity = CodeAnalysis.DiagnosticSeverity.Info, + Message = $"Writing documentation output to: {outputPath}...", + Range = null, + Source = null, + Stage = IRewriteStep.Stage.Transformation, + }); + if (!Directory.Exists(outputPath)) + { + this.TryWithExceptionsAsDiagnostics( + "creating directory", + async () => Directory.CreateDirectory(outputPath) + ).Wait(); + } } - PackageLink = PackageName == null - ? "" - : $"\nPackage: [{PackageName}](https://nuget.org/packages/{PackageName})\n"; + this.PackageLink = this.PackageName == null + ? string.Empty + : $"\nPackage: [{this.PackageName}](https://nuget.org/packages/{this.PackageName})\n"; } /// @@ -90,7 +142,7 @@ public async Task WriteOutput(QsNamespace ns, DocComment docComment) // Q# metadata ["qsharp.kind"] = "namespace", ["qsharp.name"] = name, - ["qsharp.summary"] = docComment.Summary + ["qsharp.summary"] = docComment.Summary, }; var document = $@" # {title} @@ -102,9 +154,8 @@ public async Task WriteOutput(QsNamespace ns, DocComment docComment) .WithYamlHeader(header); // Open a file to write the new doc to. - await File.WriteAllTextAsync( - Path.Join(this.OutputPath, $"{name.ToLowerInvariant()}.md"), - document + await this.WriteAllTextAsync( + name, document ); } @@ -121,6 +172,7 @@ await File.WriteAllTextAsync( public async Task WriteOutput(QsCustomType type, DocComment docComment) { var namedItemDeclarations = type.TypeItems.ToDictionaryOfDeclarations(); + // Make a new Markdown document for the type declaration. var title = $"{type.FullName.Name.Value} user defined type"; var header = new Dictionary @@ -137,18 +189,18 @@ public async Task WriteOutput(QsCustomType type, DocComment docComment) ["qsharp.kind"] = "udt", ["qsharp.namespace"] = type.FullName.Namespace.Value, ["qsharp.name"] = type.FullName.Name.Value, - ["qsharp.summary"] = docComment.Summary + ["qsharp.summary"] = docComment.Summary, }; var document = $@" +# {title} + Namespace: [{type.FullName.Namespace.Value}](xref:{type.FullName.Namespace.Value}) {this.PackageLink} -# {title} - {docComment.Summary} ```Q# -{type.ToSyntax()} +{type.WithoutDocumentationAndComments().ToSyntax()} ``` " @@ -174,8 +226,8 @@ public async Task WriteOutput(QsCustomType type, DocComment docComment) .WithYamlHeader(header); // Open a file to write the new doc to. - await File.WriteAllTextAsync( - Path.Join(this.OutputPath, $"{type.FullName.Namespace.Value.ToLowerInvariant()}.{type.FullName.Name.Value.ToLowerInvariant()}.md"), + await this.WriteAllTextAsync( + $"{type.FullName.Namespace.Value}.{type.FullName.Name.Value}.md", document ); } @@ -200,7 +252,7 @@ public async Task WriteOutput(QsCallable callable, DocComment docComment) QsCallableKind.Tags.Function => "function", QsCallableKind.Tags.Operation => "operation", QsCallableKind.Tags.TypeConstructor => "type constructor", - _ => "" + _ => "", }; var title = $@"{callable.FullName.Name.Value} {kind}"; var header = new Dictionary @@ -217,14 +269,14 @@ public async Task WriteOutput(QsCallable callable, DocComment docComment) ["qsharp.kind"] = kind, ["qsharp.namespace"] = callable.FullName.Namespace.Value, ["qsharp.name"] = callable.FullName.Name.Value, - ["qsharp.summary"] = docComment.Summary + ["qsharp.summary"] = docComment.Summary, }; var document = $@" +# {title} + Namespace: [{callable.FullName.Namespace.Value}](xref:{callable.FullName.Namespace.Value}) {this.PackageLink} -# {title} - {docComment.Summary} ```Q# @@ -260,8 +312,8 @@ public async Task WriteOutput(QsCallable callable, DocComment docComment) .WithYamlHeader(header); // Open a file to write the new doc to. - await File.WriteAllTextAsync( - Path.Join(this.OutputPath, $"{callable.FullName.Namespace.Value.ToLowerInvariant()}.{callable.FullName.Name.Value.ToLowerInvariant()}.md"), + await this.WriteAllTextAsync( + $"{callable.FullName.Namespace.Value}.{callable.FullName.Name.Value}.md", document ); } From 66d5c16fabc0b143fa9d2007bd22c3fb79ee3dce Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Thu, 15 Oct 2020 10:23:26 -0700 Subject: [PATCH 3/5] Strip comments from syntax blocks. --- .../DocumentationGenerator/Extensions.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Documentation/DocumentationGenerator/Extensions.cs b/src/Documentation/DocumentationGenerator/Extensions.cs index 7d726cf9c8..ba768ed96c 100644 --- a/src/Documentation/DocumentationGenerator/Extensions.cs +++ b/src/Documentation/DocumentationGenerator/Extensions.cs @@ -300,7 +300,8 @@ internal static Dictionary ToDictionaryOfDeclarations(this ResolvedTypeKind.Tags.Range => "Range", ResolvedTypeKind.Tags.String => "String", ResolvedTypeKind.Tags.UnitType => "Unit", - _ => "__invalid__", + ResolvedTypeKind.Tags.InvalidType => "__invalid__", + _ => $"__invalid<{type.Resolution.ToString()}>__", }, }; @@ -320,6 +321,19 @@ internal static bool IsInCompilationUnit(this QsCallable callable) => internal static bool IsInCompilationUnit(this QsCustomType type) => type.SourceFile.Value.EndsWith(".qs"); + + internal static QsCustomType WithoutDocumentationAndComments(this QsCustomType type) => + new QsCustomType( + fullName: type.FullName, + attributes: type.Attributes, + modifiers: type.Modifiers, + sourceFile: type.SourceFile, + location: type.Location, + type: type.Type, + typeItems: type.TypeItems, + documentation: ImmutableArray.Empty, + comments: QsComments.Empty + ); } } From 27a84c1f56598ba97e919e69a9756d7d63f9bfe6 Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Thu, 15 Oct 2020 10:24:06 -0700 Subject: [PATCH 4/5] Style and diagnostics fixes. --- .../DocumentationGenerator/ProcessDocComments.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Documentation/DocumentationGenerator/ProcessDocComments.cs b/src/Documentation/DocumentationGenerator/ProcessDocComments.cs index 891aadb190..a287bbd5c6 100644 --- a/src/Documentation/DocumentationGenerator/ProcessDocComments.cs +++ b/src/Documentation/DocumentationGenerator/ProcessDocComments.cs @@ -32,7 +32,7 @@ public class ProcessDocComments public class TransformationState { } - private readonly DocumentationWriter? writer; + internal readonly DocumentationWriter? Writer; /// /// Initializes a new instance of the class. @@ -51,12 +51,12 @@ public ProcessDocComments( ) : base(new TransformationState(), TransformationOptions.Disabled) { - this.writer = outputPath == null + this.Writer = outputPath == null ? null : new DocumentationWriter(outputPath, packageName); // We provide our own custom namespace transformation, and expression kind transformation. - this.Namespaces = new ProcessDocComments.NamespaceTransformation(this, this.writer); + this.Namespaces = new ProcessDocComments.NamespaceTransformation(this, this.Writer); } private class NamespaceTransformation From 4c657008b6f1e0845663a3c8fd8c71b3849bb1e7 Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Thu, 15 Oct 2020 10:25:00 -0700 Subject: [PATCH 5/5] Fix #679. --- src/QsCompiler/CommandLineTool/Commands/Build.cs | 10 +++++++++- src/QsCompiler/CommandLineTool/Options.cs | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/QsCompiler/CommandLineTool/Commands/Build.cs b/src/QsCompiler/CommandLineTool/Commands/Build.cs index c91c1c5727..56b24d76c2 100644 --- a/src/QsCompiler/CommandLineTool/Commands/Build.cs +++ b/src/QsCompiler/CommandLineTool/Commands/Build.cs @@ -112,8 +112,14 @@ internal static bool IncorporateResponseFiles(BuildOptions options, out BuildOpt /// private static IEnumerable SplitCommandLineArguments(string commandLine) { + string TrimQuotes(string s) => + s.StartsWith('"') && s.EndsWith('"') + ? s.Substring(1, s.Length - 2) + : s; + var parmChars = commandLine?.ToCharArray() ?? Array.Empty(); var inQuote = false; + for (int index = 0; index < parmChars.Length; index++) { var precededByBackslash = index > 0 && parmChars[index - 1] == '\\'; @@ -131,9 +137,10 @@ private static IEnumerable SplitCommandLineArguments(string commandLine) parmChars[index] = '\n'; } } + return new string(parmChars) .Split('\n', StringSplitOptions.RemoveEmptyEntries) - .Select(arg => arg.Trim('"')); + .Select(arg => TrimQuotes(arg)); } /// @@ -148,6 +155,7 @@ private static BuildOptions FromResponseFiles(IEnumerable responseFiles) throw new ArgumentNullException(nameof(responseFiles)); } var commandLine = string.Join(" ", responseFiles.Select(File.ReadAllText)); + System.Diagnostics.Debugger.Launch(); var args = SplitCommandLineArguments(commandLine); var parsed = Parser.Default.ParseArguments(args); return parsed.MapResult( diff --git a/src/QsCompiler/CommandLineTool/Options.cs b/src/QsCompiler/CommandLineTool/Options.cs index de79138fe8..3d3402ed1c 100644 --- a/src/QsCompiler/CommandLineTool/Options.cs +++ b/src/QsCompiler/CommandLineTool/Options.cs @@ -90,7 +90,8 @@ internal bool ParseAssemblyProperties(out Dictionary parsed) parsed = new Dictionary(); foreach (var keyValue in this.AdditionalAssemblyProperties ?? Array.Empty()) { - var pieces = keyValue?.Split(":"); + // NB: We use `count: 2` here to ensure that assembly constants can contain colons. + var pieces = keyValue?.Split(":", count: 2); var valid = pieces != null && pieces.Length == 2; success = valid && parsed.TryAdd(pieces[0].Trim().Trim('"'), pieces[1].Trim().Trim('"')) && success; }