From 02074f0020ed272df2cbc5182e18dcf8affdc5a9 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Thu, 5 Feb 2015 13:30:38 -0600 Subject: [PATCH 01/12] Render as Markdown analyzer and code fix --- LICENSE.md | 36 +- .../DocumentationCommentPrinter.cs | 605 ++++++++++++++++++ .../DocumentationCommentTextWriter.cs | 170 +++++ .../DocumentationSyntaxExtensions.cs | 11 + .../OpenStackNetAnalyzers.csproj | 7 + .../RenderAsMarkdownAnalyzer.cs | 47 ++ .../RenderAsMarkdownCodeFix.cs | 173 +++++ .../OpenStackNetAnalyzers/XmlSyntaxFactory.cs | 19 +- .../OpenStackNetAnalyzers/packages.config | 1 + 9 files changed, 1066 insertions(+), 3 deletions(-) create mode 100644 OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationCommentPrinter.cs create mode 100644 OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationCommentTextWriter.cs create mode 100644 OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs create mode 100644 OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs diff --git a/LICENSE.md b/LICENSE.md index 3a3c723..a698084 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) Rackspace, US Inc. All rights reserved. +Copyright (c) Rackspace, US Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use these files except in compliance with the License. You may obtain a copy of the @@ -10,3 +10,37 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +## Third Party Licenses + +### CommonMark.NET + +The CommonMark.NET library is used under the following license: + +> Copyright (c) 2014, Kārlis Gaņģis +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> * Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> +> * Redistributions in binary form must reproduce the above copyright +> notice, this list of conditions and the following disclaimer in the +> documentation and/or other materials provided with the distribution. +> +> * Neither the name of Kārlis Gaņģis nor the names of other contributors +> may be used to endorse or promote products derived from this software +> without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +> DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationCommentPrinter.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationCommentPrinter.cs new file mode 100644 index 0000000..5a7ae5e --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationCommentPrinter.cs @@ -0,0 +1,605 @@ +namespace OpenStackNetAnalyzers +{ + using System.Collections.Generic; + using System.Globalization; + using System.Text; + using CommonMark; + using CommonMark.Syntax; + + internal static class DocumentationCommentPrinter + { + private static readonly char[] EscapeHtmlCharacters = new[] { '&', '<', '>', '"' }; + private const string HexCharacters = "0123456789ABCDEF"; + + private static readonly char[] EscapeHtmlLessThan = "<".ToCharArray(); + private static readonly char[] EscapeHtmlGreaterThan = ">".ToCharArray(); + private static readonly char[] EscapeHtmlAmpersand = "&".ToCharArray(); + private static readonly char[] EscapeHtmlQuote = """.ToCharArray(); + + private static readonly string[] HeaderOpenerTags = new[] { "

", "

", "

", "

", "

", "
" }; + private static readonly string[] HeaderCloserTags = new[] { "
", "", "", "", "", "" }; + + private static readonly bool[] UrlSafeCharacters = new[] { + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, true, false, true, true, true, false, false, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, false, true, false, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, true, + false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, + }; + + /// + /// Escapes special URL characters. + /// + /// Orig: escape_html(inp, preserve_entities) + private static void EscapeUrl(string input, DocumentationCommentTextWriter target) + { + if (input == null) + return; + + char c; + int lastPos = 0; + int len = input.Length; + char[] buffer; + + if (target.Buffer.Length < len) + buffer = target.Buffer = input.ToCharArray(); + else + { + buffer = target.Buffer; + input.CopyTo(0, buffer, 0, len); + } + + // since both \r and \n are not url-safe characters and will be encoded, all calls are + // made to WriteConstant. + for (var pos = 0; pos < len; pos++) + { + c = buffer[pos]; + + if (c == '&') + { + target.WriteConstant(buffer, lastPos, pos - lastPos); + lastPos = pos + 1; + target.WriteConstant(EscapeHtmlAmpersand); + } + else if (c < 128 && !UrlSafeCharacters[c]) + { + target.WriteConstant(buffer, lastPos, pos - lastPos); + lastPos = pos + 1; + + target.WriteConstant(new[] { '%', HexCharacters[c / 16], HexCharacters[c % 16] }); + } + else if (c > 127) + { + target.WriteConstant(buffer, lastPos, pos - lastPos); + lastPos = pos + 1; + + byte[] bytes; + if (c >= '\ud800' && c <= '\udfff' && len != lastPos) + { + // this char is the first of UTF-32 character pair + bytes = Encoding.UTF8.GetBytes(new[] { c, buffer[lastPos] }); + lastPos = ++pos + 1; + } + else + { + bytes = Encoding.UTF8.GetBytes(new[] { c }); + } + + for (var i = 0; i < bytes.Length; i++) + target.WriteConstant(new[] { '%', HexCharacters[bytes[i] / 16], HexCharacters[bytes[i] % 16] }); + } + } + + target.WriteConstant(buffer, lastPos, len - lastPos); + } + + /// + /// Escapes special HTML characters. + /// + /// Orig: escape_html(inp, preserve_entities) + private static void EscapeHtml(string input, DocumentationCommentTextWriter target) + { + if (input.Length == 0) + return; + + int pos; + int lastPos = 0; + char[] buffer; + + if (target.Buffer.Length < input.Length) + buffer = target.Buffer = new char[input.Length]; + else + buffer = target.Buffer; + + input.CopyTo(0, buffer, 0, input.Length); + + while ((pos = input.IndexOfAny(EscapeHtmlCharacters, lastPos, input.Length - lastPos + 0)) != -1) + { + target.Write(buffer, lastPos - 0, pos - lastPos); + lastPos = pos + 1; + + switch (input[pos]) + { + case '<': + target.WriteConstant(EscapeHtmlLessThan); + break; + case '>': + target.WriteConstant(EscapeHtmlGreaterThan); + break; + case '&': + target.WriteConstant(EscapeHtmlAmpersand); + break; + case '"': + target.WriteConstant(EscapeHtmlQuote); + break; + } + } + + target.Write(buffer, lastPos - 0, input.Length - lastPos + 0); + } + + /// + /// Escapes special HTML characters. + /// + /// Orig: escape_html(inp, preserve_entities) + private static void EscapeHtml(StringContent inp, DocumentationCommentTextWriter target) + { + int pos; + int lastPos; + char[] buffer = target.Buffer; + + var part = inp.ToString(new StringBuilder()); + + if (buffer.Length < part.Length) + buffer = target.Buffer = new char[part.Length]; + + part.CopyTo(0, buffer, 0, part.Length); + + lastPos = pos = 0; + while ((pos = part.IndexOfAny(EscapeHtmlCharacters, lastPos, part.Length - lastPos + 0)) != -1) + { + target.Write(buffer, lastPos - 0, pos - lastPos); + lastPos = pos + 1; + + switch (part[pos]) + { + case '<': + target.WriteConstant(EscapeHtmlLessThan); + break; + case '>': + target.WriteConstant(EscapeHtmlGreaterThan); + break; + case '&': + target.WriteConstant(EscapeHtmlAmpersand); + break; + case '"': + target.WriteConstant(EscapeHtmlQuote); + break; + } + } + + target.Write(buffer, lastPos - 0, part.Length - lastPos + 0); + } + + /// + /// Convert a block list to HTML. Returns 0 on success, and sets result. + /// + /// Orig: blocks_to_html + public static void BlocksToHtml(System.IO.TextWriter writer, Block block, CommonMarkSettings settings) + { + var wrapper = new DocumentationCommentTextWriter(writer); + BlocksToHtmlInner(wrapper, block, settings); + } + + private static void BlocksToHtmlInner(DocumentationCommentTextWriter writer, Block block, CommonMarkSettings settings) + { + var stack = new Stack(); + var inlineStack = new Stack(); + bool visitChildren; + string stackLiteral = null; + bool stackTight = false; + bool tight = false; + int x; + + while (block != null) + { + visitChildren = false; + + switch (block.Tag) + { + case BlockTag.Document: + stackLiteral = null; + stackTight = false; + visitChildren = true; + break; + + case BlockTag.Paragraph: + if (tight) + { + InlinesToHtml(writer, block.InlineContent, settings, inlineStack); + } + else + { + writer.EnsureLine(); + writer.WriteConstant(""); + InlinesToHtml(writer, block.InlineContent, settings, inlineStack); + writer.WriteLineConstant(""); + } + break; + + case BlockTag.BlockQuote: + writer.EnsureLine(); + writer.WriteLineConstant(""); + + stackLiteral = ""; + stackTight = false; + visitChildren = true; + break; + + case BlockTag.ListItem: + writer.EnsureLine(); + writer.WriteConstant(""); + + stackLiteral = ""; + stackTight = tight; + visitChildren = true; + break; + + case BlockTag.List: + // make sure a list starts at the beginning of the line: + writer.EnsureLine(); + var data = block.ListData; + writer.WriteConstant(data.ListType == ListType.Bullet ? ""); + + stackLiteral = ""; + stackTight = data.IsTight; + visitChildren = true; + break; + + case BlockTag.AtxHeader: + case BlockTag.SETextHeader: + writer.EnsureLine(); + + x = block.HeaderLevel; + writer.WriteConstant(x > 0 && x < 7 ? HeaderOpenerTags[x - 1] : ""); + InlinesToHtml(writer, block.InlineContent, settings, inlineStack); + writer.WriteLineConstant(x > 0 && x < 7 ? HeaderCloserTags[x - 1] : ""); + break; + + case BlockTag.IndentedCode: + writer.EnsureLine(); + writer.WriteConstant(""); + EscapeHtml(block.StringContent, writer); + writer.WriteLineConstant(""); + break; + + case BlockTag.FencedCode: + writer.EnsureLine(); + writer.WriteConstant(" 0) + { + x = info.IndexOf(' '); + if (x == -1) + x = info.Length; + + writer.WriteConstant(" language=\""); + EscapeHtml(info.Substring(0, x), writer); + writer.Write('\"'); + } + writer.Write('>'); + writer.WriteLine(); + EscapeHtml(block.StringContent, writer); + writer.WriteLineConstant(""); + break; + + case BlockTag.HtmlBlock: + writer.Write(block.StringContent.ToString(new StringBuilder())); + break; + + case BlockTag.HorizontalRuler: + writer.WriteLineConstant("
"); + break; + + case BlockTag.ReferenceDefinition: + break; + + default: + throw new CommonMarkException("Block type " + block.Tag + " is not supported.", block); + } + + if (visitChildren) + { + stack.Push(new BlockStackEntry(stackLiteral, block.NextSibling, tight)); + + tight = stackTight; + block = block.FirstChild; + } + else if (block.NextSibling != null) + { + block = block.NextSibling; + } + else + { + block = null; + } + + while (block == null && stack.Count > 0) + { + var entry = stack.Pop(); + + writer.WriteLineConstant(entry.Literal); + tight = entry.IsTight; + block = entry.Target; + } + } + } + + /// + /// Writes the inline list to the given writer as plain text (without any HTML tags). + /// + /// + private static void InlinesToPlainText(DocumentationCommentTextWriter writer, Inline inline, Stack stack) + { + bool withinLink = false; + bool stackWithinLink = false; + bool visitChildren; + string stackLiteral = null; + var origStackCount = stack.Count; + + while (inline != null) + { + visitChildren = false; + + switch (inline.Tag) + { + case InlineTag.String: + case InlineTag.Code: + case InlineTag.RawHtml: + EscapeHtml(inline.LiteralContent, writer); + break; + + case InlineTag.LineBreak: + case InlineTag.SoftBreak: + writer.WriteLine(); + break; + + case InlineTag.Link: + if (withinLink) + { + writer.Write('['); + stackLiteral = "]"; + visitChildren = true; + stackWithinLink = withinLink; + } + else + { + visitChildren = true; + stackWithinLink = true; + stackLiteral = string.Empty; + } + break; + + case InlineTag.Image: + visitChildren = true; + stackWithinLink = true; + stackLiteral = string.Empty; + break; + + case InlineTag.Strong: + case InlineTag.Emphasis: + stackLiteral = string.Empty; + stackWithinLink = withinLink; + visitChildren = true; + break; + + default: + throw new CommonMarkException("Inline type " + inline.Tag + " is not supported.", inline); + } + + if (visitChildren) + { + stack.Push(new InlineStackEntry(stackLiteral, inline.NextSibling, withinLink)); + + withinLink = stackWithinLink; + inline = inline.FirstChild; + } + else if (inline.NextSibling != null) + { + inline = inline.NextSibling; + } + else + { + inline = null; + } + + while (inline == null && stack.Count > origStackCount) + { + var entry = stack.Pop(); + writer.WriteConstant(entry.Literal); + inline = entry.Target; + withinLink = entry.IsWithinLink; + } + } + } + + /// + /// Writes the inline list to the given writer as HTML code. + /// + private static void InlinesToHtml(DocumentationCommentTextWriter writer, Inline inline, CommonMarkSettings settings, Stack stack) + { + var uriResolver = settings.UriResolver; + bool withinLink = false; + bool stackWithinLink = false; + bool visitChildren; + string stackLiteral = null; + + while (inline != null) + { + visitChildren = false; + + switch (inline.Tag) + { + case InlineTag.String: + EscapeHtml(inline.LiteralContent, writer); + break; + + case InlineTag.LineBreak: + writer.WriteLineConstant("
"); + break; + + case InlineTag.SoftBreak: + if (settings.RenderSoftLineBreaksAsLineBreaks) + writer.WriteLineConstant("
"); + else + writer.WriteLine(); + break; + + case InlineTag.Code: + writer.WriteConstant(""); + EscapeHtml(inline.LiteralContent, writer); + writer.WriteConstant(""); + break; + + case InlineTag.RawHtml: + writer.Write(inline.LiteralContent); + break; + + case InlineTag.Link: + if (withinLink) + { + writer.Write('['); + stackLiteral = "]"; + stackWithinLink = withinLink; + visitChildren = true; + } + else + { + writer.WriteConstant(" 0) + { + writer.WriteConstant(" title=\""); + EscapeHtml(inline.LiteralContent, writer); + writer.Write('\"'); + } + + writer.Write('>'); + + visitChildren = true; + stackWithinLink = true; + stackLiteral = ""; + } + break; + + case InlineTag.Image: + writer.WriteConstant("\""); 0) + { + writer.WriteConstant(" title=\""); + EscapeHtml(inline.LiteralContent, writer); + writer.Write('\"'); + } + writer.WriteConstant(" />"); + break; + + case InlineTag.Strong: + writer.WriteConstant(""); + stackLiteral = ""; + stackWithinLink = withinLink; + visitChildren = true; + break; + + case InlineTag.Emphasis: + writer.WriteConstant(""); + stackLiteral = ""; + visitChildren = true; + stackWithinLink = withinLink; + break; + + case InlineTag.Strikethrough: + writer.WriteConstant(""); + stackLiteral = ""; + visitChildren = true; + stackWithinLink = withinLink; + break; + + default: + throw new CommonMarkException("Inline type " + inline.Tag + " is not supported.", inline); + } + + if (visitChildren) + { + stack.Push(new InlineStackEntry(stackLiteral, inline.NextSibling, withinLink)); + + withinLink = stackWithinLink; + inline = inline.FirstChild; + } + else if (inline.NextSibling != null) + { + inline = inline.NextSibling; + } + else + { + inline = null; + } + + while (inline == null && stack.Count > 0) + { + var entry = stack.Pop(); + writer.WriteConstant(entry.Literal); + inline = entry.Target; + withinLink = entry.IsWithinLink; + } + } + } + + private struct BlockStackEntry + { + public readonly string Literal; + public readonly Block Target; + public readonly bool IsTight; + public BlockStackEntry(string literal, Block target, bool isTight) + { + this.Literal = literal; + this.Target = target; + this.IsTight = isTight; + } + } + private struct InlineStackEntry + { + public readonly string Literal; + public readonly Inline Target; + public readonly bool IsWithinLink; + public InlineStackEntry(string literal, Inline target, bool isWithinLink) + { + this.Literal = literal; + this.Target = target; + this.IsWithinLink = isWithinLink; + } + } + } +} diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationCommentTextWriter.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationCommentTextWriter.cs new file mode 100644 index 0000000..e719293 --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationCommentTextWriter.cs @@ -0,0 +1,170 @@ +namespace OpenStackNetAnalyzers +{ + /// + /// A wrapper for that keeps track if the last symbol has been a newline. + /// + internal sealed class DocumentationCommentTextWriter + { + private System.IO.TextWriter _inner; + private char _last = '\n'; + private bool _windowsNewLine; + private char[] _newline; + + /// + /// A reusable char buffer. This is used internally in (and thus will modify the buffer) + /// but can also be used from class. + /// + internal char[] Buffer = new char[256]; + + public DocumentationCommentTextWriter(System.IO.TextWriter inner) + { + _inner = inner; + + var nl = inner.NewLine; + _newline = nl.ToCharArray(); + _windowsNewLine = nl == "\r\n"; + } + + public void WriteLine() + { + _inner.Write(_newline); + _last = '\n'; + } + + public void Write(string value) + { + if (value.Length == 0) + return; + + if (Buffer.Length < value.Length) + Buffer = new char[value.Length]; + + value.CopyTo(0, Buffer, 0, value.Length); + + if (_windowsNewLine) + { + var lastPos = 0; + var pos = lastPos; + var lastC = _last; + + while (-1 != (pos = value.IndexOf('\n', pos, value.Length - pos + 0))) + { + lastC = pos == 0 ? _last : value[pos - 1]; + + if (lastC != '\r') + { + _inner.Write(Buffer, lastPos - 0, pos - lastPos); + _inner.Write('\r'); + lastPos = pos; + } + + pos++; + } + + _inner.Write(Buffer, lastPos - 0, value.Length - lastPos + 0); + } + else + { + _inner.Write(Buffer, 0, value.Length); + } + + _last = Buffer[value.Length - 1]; + } + + /// + /// Writes a value that is known not to contain any newlines. + /// + public void WriteConstant(char[] value) + { + _last = 'c'; + _inner.Write(value, 0, value.Length); + } + + /// + /// Writes a value that is known not to contain any newlines. + /// + public void WriteConstant(char[] value, int startIndex, int length) + { + _last = 'c'; + _inner.Write(value, startIndex, length); + } + + /// + /// Writes a value that is known not to contain any newlines. + /// + public void WriteConstant(string value) + { + _last = 'c'; + _inner.Write(value); + } + + /// + /// Writes a value that is known not to contain any newlines. + /// + public void WriteLineConstant(string value) + { + _last = '\n'; + _inner.Write(value); + _inner.Write(_newline); + } + + public void Write(char[] value, int index, int count) + { + if (value == null || count == 0) + return; + + if (_windowsNewLine) + { + var lastPos = index; + var lastC = _last; + int pos = index; + + while (pos < index + count) + { + if (value[pos] != '\n') + { + pos++; + continue; + } + + lastC = pos == index ? _last : value[pos - 1]; + + if (lastC != '\r') + { + _inner.Write(value, lastPos, pos - lastPos); + _inner.Write('\r'); + lastPos = pos; + } + + pos++; + } + + _inner.Write(value, lastPos, index + count - lastPos); + } + else + { + _inner.Write(value, index, count); + } + + _last = value[index + count - 1]; + } + + public void Write(char value) + { + if (_windowsNewLine && _last != '\r' && value == '\n') + _inner.Write('\r'); + + _last = value; + _inner.Write(value); + } + + /// + /// Adds a newline if the writer does not currently end with a newline. + /// + public void EnsureLine() + { + if (_last != '\n') + WriteLine(); + } + } +} diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationSyntaxExtensions.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationSyntaxExtensions.cs index 773eaad..81873d0 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationSyntaxExtensions.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationSyntaxExtensions.cs @@ -50,8 +50,19 @@ public static IEnumerable GetXmlElements(this SyntaxList(this T node, SyntaxTrivia trivia) where T : XmlNodeSyntax + { + return node.ReplaceExteriorTriviaImpl(trivia); + } + + private static T ReplaceExteriorTriviaImpl(this T node, SyntaxTrivia trivia) + where T : SyntaxNode { // Make sure to include a space after the '///' characters. SyntaxTrivia triviaWithSpace = SyntaxFactory.DocumentationCommentExterior(trivia.ToString() + " "); diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj index 189be22..dabc001 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj @@ -33,6 +33,8 @@ + + @@ -48,6 +50,8 @@ + + @@ -68,6 +72,9 @@ + + ..\..\packages\CommonMark.NET.0.8.0\lib\portable-net40+sl50+wp80+win+wpa81+MonoAndroid10+MonoTouch10\CommonMark.dll + ..\..\packages\Microsoft.CodeAnalysis.Common.1.0.0.0-beta2\lib\portable-net45+win8\Microsoft.CodeAnalysis.dll False diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs new file mode 100644 index 0000000..fbcaf60 --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs @@ -0,0 +1,47 @@ +namespace OpenStackNetAnalyzers +{ + using System.Collections.Immutable; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class RenderAsMarkdownAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "RenderAsMarkdown"; + internal const string Title = "Render documentation as Markdown (Refactoring)"; + internal const string MessageFormat = "Render documentation as Markdown"; + internal const string Category = "OpenStack.Documentation"; + internal const string Description = "Render documentation as Markdown (Refactoring)"; + + private static DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Hidden, isEnabledByDefault: true, description: Description); + + private static readonly ImmutableArray _supportedDiagnostics = + ImmutableArray.Create(Descriptor); + + public override ImmutableArray SupportedDiagnostics + { + get + { + return _supportedDiagnostics; + } + } + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.PropertyDeclaration); + } + + private void HandleDocumentedNode(SyntaxNodeAnalysisContext context) + { + DocumentationCommentTriviaSyntax documentationComment = context.Node.GetDocumentationCommentTriviaSyntax(); + if (documentationComment == null) + return; + + // only report the diagnostic for elements which have documentation comments + context.ReportDiagnostic(Diagnostic.Create(Descriptor, documentationComment.GetLocation())); + } + } +} diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs new file mode 100644 index 0000000..199c251 --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -0,0 +1,173 @@ +namespace OpenStackNetAnalyzers +{ + using System; + using System.Collections.Immutable; + using System.Composition; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using CommonMark; + using CommonMark.Syntax; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeActions; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + [ExportCodeFixProvider(nameof(RenderAsMarkdownCodeFix), LanguageNames.CSharp)] + [Shared] + public class RenderAsMarkdownCodeFix : CodeFixProvider + { + private static readonly ImmutableArray _fixableDiagnostics = + ImmutableArray.Create(RenderAsMarkdownAnalyzer.DiagnosticId); + + public sealed override ImmutableArray GetFixableDiagnosticIds() + { + return _fixableDiagnostics; + } + + public override FixAllProvider GetFixAllProvider() + { + // this is unlikely to work as expected + return null; + } + + public override async Task ComputeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + if (!string.Equals(diagnostic.Id, RenderAsMarkdownAnalyzer.DiagnosticId, StringComparison.Ordinal)) + continue; + + var documentRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken); + SyntaxNode syntax = documentRoot.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true); + if (syntax == null) + continue; + + DocumentationCommentTriviaSyntax documentationCommentTriviaSyntax = syntax.FirstAncestorOrSelf(); + if (documentationCommentTriviaSyntax == null) + continue; + + string description = "Render documentation as Markdown"; + context.RegisterFix(CodeAction.Create(description, cancellationToken => CreateChangedDocument(context, documentationCommentTriviaSyntax, cancellationToken)), diagnostic); + } + } + + private async Task CreateChangedDocument(CodeFixContext context, DocumentationCommentTriviaSyntax documentationCommentTriviaSyntax, CancellationToken cancellationToken) + { + StringBuilder leadingTriviaBuilder = new StringBuilder(); + SyntaxToken parentToken = documentationCommentTriviaSyntax.ParentTrivia.Token; + int documentationCommentIndex = parentToken.LeadingTrivia.IndexOf(documentationCommentTriviaSyntax.ParentTrivia); + for (int i = 0; i < documentationCommentIndex; i++) + { + SyntaxTrivia trivia = parentToken.LeadingTrivia[i]; + switch (trivia.CSharpKind()) + { + case SyntaxKind.EndOfLineTrivia: + leadingTriviaBuilder.Clear(); + break; + + case SyntaxKind.WhitespaceTrivia: + leadingTriviaBuilder.Append(trivia.ToFullString()); + break; + + default: + break; + } + } + + leadingTriviaBuilder.Append(documentationCommentTriviaSyntax.GetLeadingTrivia().ToFullString()); + + // this is the trivia that should appear at the beginning of each line of the comment. + SyntaxTrivia leadingTrivia = SyntaxFactory.DocumentationCommentExterior(leadingTriviaBuilder.ToString()); + + DocumentationCommentTriviaSyntax contentsOnly = RemoveExteriorTrivia(documentationCommentTriviaSyntax); + contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes(), RenderBlockElementAsMarkdown); + string renderedContent = contentsOnly.Content.ToFullString(); + string[] lines = renderedContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + SyntaxList newContent = XmlSyntaxFactory.List(); + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + if (string.IsNullOrWhiteSpace(line)) + { + if (i == lines.Length - 1) + break; + + line = string.Empty; + } + + if (newContent.Count > 0) + newContent = newContent.Add(XmlSyntaxFactory.NewLine()); + + newContent = newContent.Add(XmlSyntaxFactory.Text(line.TrimEnd(), true)); + } + + contentsOnly = contentsOnly + .WithContent(newContent) + .WithLeadingTrivia(SyntaxFactory.DocumentationCommentExterior("///")) + .WithTrailingTrivia(SyntaxFactory.EndOfLine(Environment.NewLine)); + + contentsOnly = + contentsOnly + .ReplaceExteriorTrivia(leadingTrivia) + .WithLeadingTrivia(SyntaxFactory.DocumentationCommentExterior("///")); + + SyntaxNode root = await context.Document.GetSyntaxRootAsync(cancellationToken); + SyntaxNode newRoot = root.ReplaceNode(documentationCommentTriviaSyntax, contentsOnly); + return context.Document.WithSyntaxRoot(newRoot); + } + + private SyntaxNode RenderBlockElementAsMarkdown(SyntaxNode originalNode, SyntaxNode rewrittenNode) + { + XmlElementSyntax elementSyntax = rewrittenNode as XmlElementSyntax; + if (elementSyntax == null) + return rewrittenNode; + + switch (elementSyntax.StartTag?.Name?.ToString()) + { + case "summary": + case "remarks": + case "returns": + case "value": + break; + + default: + return rewrittenNode; + } + + string rendered = RenderAsMarkdown(elementSyntax.Content.ToString()).Trim(); + return elementSyntax.WithContent( + XmlSyntaxFactory.List( + XmlSyntaxFactory.NewLine().WithoutTrailingTrivia(), + XmlSyntaxFactory.Text(rendered, true), + XmlSyntaxFactory.NewLine().WithoutTrailingTrivia())); + } + + private string RenderAsMarkdown(string text) + { + Block document; + using (System.IO.StringReader reader = new System.IO.StringReader(text)) + { + document = CommonMarkConverter.ProcessStage1(reader, CommonMarkSettings.Default); + CommonMarkConverter.ProcessStage2(document, CommonMarkSettings.Default); + } + + StringBuilder builder = new StringBuilder(); + using (System.IO.StringWriter writer = new System.IO.StringWriter(builder)) + { + DocumentationCommentPrinter.BlocksToHtml(writer, document, CommonMarkSettings.Default); + } + + return builder.ToString(); + } + + private DocumentationCommentTriviaSyntax RemoveExteriorTrivia(DocumentationCommentTriviaSyntax documentationComment) + { + return documentationComment.ReplaceTrivia( + documentationComment.DescendantTrivia(descendIntoTrivia: true).Where(i => i.IsKind(SyntaxKind.DocumentationCommentExteriorTrivia)), + (originalTrivia, rewrittenTrivia) => default(SyntaxTrivia)); + } + } +} diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/XmlSyntaxFactory.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/XmlSyntaxFactory.cs index 1d4a0fe..19b1b10 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/XmlSyntaxFactory.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/XmlSyntaxFactory.cs @@ -53,7 +53,12 @@ public static SyntaxList List(params XmlNodeSyntax[] nodes) public static XmlTextSyntax Text(string value) { - return Text(TextLiteral(value)); + return Text(value, false); + } + + public static XmlTextSyntax Text(string value, bool raw) + { + return Text(TextLiteral(value, raw)); } public static XmlTextSyntax Text(params SyntaxToken[] textTokens) @@ -138,7 +143,17 @@ public static SyntaxToken TextNewLine(bool continueComment) public static SyntaxToken TextLiteral(string value) { - string encoded = new XText(value).ToString(); + return TextLiteral(value, false); + } + + public static SyntaxToken TextLiteral(string value, bool raw) + { + string encoded; + if (raw) + encoded = value; + else + encoded = new XText(value).ToString(); + return SyntaxFactory.XmlTextLiteral( SyntaxFactory.TriviaList(), encoded, diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/packages.config b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/packages.config index 4ef9474..b8311c8 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/packages.config +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/packages.config @@ -1,5 +1,6 @@  + From 33a3eabcc5cc4ee8722101869445ae51d899702f Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Thu, 5 Feb 2015 17:02:11 -0600 Subject: [PATCH 02/12] Parse the markdown to ensure the syntax structure is correct prior to transformations --- .../OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index 199c251..6690a94 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -109,6 +109,13 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum .WithLeadingTrivia(SyntaxFactory.DocumentationCommentExterior("///")) .WithTrailingTrivia(SyntaxFactory.EndOfLine(Environment.NewLine)); + string fullContent = contentsOnly.ToFullString(); + SyntaxTriviaList parsedTrivia = SyntaxFactory.ParseLeadingTrivia(fullContent); + SyntaxTrivia documentationTrivia = parsedTrivia.FirstOrDefault(i => i.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)); + contentsOnly = documentationTrivia.GetStructure() as DocumentationCommentTriviaSyntax; + if (contentsOnly == null) + return context.Document; + contentsOnly = contentsOnly .ReplaceExteriorTrivia(leadingTrivia) From 5f93ca38c8be3c3aaab804f0e6a4752528867322 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Thu, 5 Feb 2015 17:02:48 -0600 Subject: [PATCH 03/12] Make sure to not return an equivalent (but not equal) syntax tree --- .../OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index 6690a94..73b5d68 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -123,6 +123,9 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum SyntaxNode root = await context.Document.GetSyntaxRootAsync(cancellationToken); SyntaxNode newRoot = root.ReplaceNode(documentationCommentTriviaSyntax, contentsOnly); + if (documentationCommentTriviaSyntax.IsEquivalentTo(contentsOnly)) + return context.Document; + return context.Document.WithSyntaxRoot(newRoot); } From dbd50c7cca229b3ff856aa7b7fce1ef66e51cb78 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Thu, 5 Feb 2015 17:03:20 -0600 Subject: [PATCH 04/12] Remove unnecessary nested paragraphs from the markdown output --- .../RenderAsMarkdownCodeFix.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index 73b5d68..642cb31 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -121,6 +121,9 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum .ReplaceExteriorTrivia(leadingTrivia) .WithLeadingTrivia(SyntaxFactory.DocumentationCommentExterior("///")); + // Remove unnecessary nested paragraph elements + contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes().OfType(), RemoveNestedParagraphs); + SyntaxNode root = await context.Document.GetSyntaxRootAsync(cancellationToken); SyntaxNode newRoot = root.ReplaceNode(documentationCommentTriviaSyntax, contentsOnly); if (documentationCommentTriviaSyntax.IsEquivalentTo(contentsOnly)) @@ -129,6 +132,45 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum return context.Document.WithSyntaxRoot(newRoot); } + private SyntaxNode RemoveNestedParagraphs(SyntaxNode originalNode, SyntaxNode rewrittenNode) + { + XmlElementSyntax elementSyntax = rewrittenNode as XmlElementSyntax; + if (!IsUnnecessaryParaElement(elementSyntax)) + return rewrittenNode; + + return elementSyntax.Content[0].WithTriviaFrom(rewrittenNode); + } + + private static bool IsUnnecessaryParaElement(XmlElementSyntax elementSyntax) + { + if (elementSyntax == null) + return false; + + if (HasAttributes(elementSyntax)) + return false; + + if (!IsParaElement(elementSyntax)) + return false; + + if (elementSyntax.Content.Count != 1) + return false; + + if (!IsParaElement(elementSyntax.Content[0] as XmlElementSyntax)) + return false; + + return true; + } + + private static bool HasAttributes(XmlElementSyntax syntax) + { + return syntax.StartTag?.Attributes.Count > 0; + } + + private static bool IsParaElement(XmlElementSyntax syntax) + { + return string.Equals("para", syntax?.StartTag?.Name?.ToString(), StringComparison.Ordinal); + } + private SyntaxNode RenderBlockElementAsMarkdown(SyntaxNode originalNode, SyntaxNode rewrittenNode) { XmlElementSyntax elementSyntax = rewrittenNode as XmlElementSyntax; From 0ad2e68cd897015b39dd943f38cca4a2eec275c8 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Thu, 5 Feb 2015 17:03:41 -0600 Subject: [PATCH 05/12] Enable Markdown processing for documentation comments on many other language elements --- .../RenderAsMarkdownAnalyzer.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs index fbcaf60..ad5cda5 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs @@ -31,7 +31,23 @@ public override ImmutableArray SupportedDiagnostics public override void Initialize(AnalysisContext context) { + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.ConstructorDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.ConversionOperatorDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.DelegateDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.DestructorDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.EnumDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.EnumMemberDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.EventDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.EventFieldDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.FieldDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.IndexerDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.InterfaceDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.MethodDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.NamespaceDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.OperatorDeclaration); context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.PropertyDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.StructDeclaration); } private void HandleDocumentedNode(SyntaxNodeAnalysisContext context) From 33aaba03da58dfc5631c15548ffa89c9be8cd7b5 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Thu, 5 Feb 2015 17:35:18 -0600 Subject: [PATCH 06/12] Only need to render top-level elements as markdown --- .../OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index 642cb31..362832f 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -83,7 +83,7 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum SyntaxTrivia leadingTrivia = SyntaxFactory.DocumentationCommentExterior(leadingTriviaBuilder.ToString()); DocumentationCommentTriviaSyntax contentsOnly = RemoveExteriorTrivia(documentationCommentTriviaSyntax); - contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes(), RenderBlockElementAsMarkdown); + contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.ChildNodes(), RenderBlockElementAsMarkdown); string renderedContent = contentsOnly.Content.ToFullString(); string[] lines = renderedContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); SyntaxList newContent = XmlSyntaxFactory.List(); From 6f6e869c8bb54f07295a8aa791abe89e1e736d79 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Thu, 5 Feb 2015 17:35:56 -0600 Subject: [PATCH 07/12] Fix additional spaces getting inserted before element start tags --- .../RenderAsMarkdownCodeFix.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index 362832f..b20bff7 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -99,13 +99,15 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum } if (newContent.Count > 0) - newContent = newContent.Add(XmlSyntaxFactory.NewLine()); + newContent = newContent.Add(XmlSyntaxFactory.NewLine().WithTrailingTrivia(SyntaxFactory.DocumentationCommentExterior("///"))); newContent = newContent.Add(XmlSyntaxFactory.Text(line.TrimEnd(), true)); } - contentsOnly = contentsOnly - .WithContent(newContent) + contentsOnly = contentsOnly.WithContent(newContent); + contentsOnly = + contentsOnly + .ReplaceExteriorTrivia(leadingTrivia) .WithLeadingTrivia(SyntaxFactory.DocumentationCommentExterior("///")) .WithTrailingTrivia(SyntaxFactory.EndOfLine(Environment.NewLine)); @@ -116,11 +118,6 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum if (contentsOnly == null) return context.Document; - contentsOnly = - contentsOnly - .ReplaceExteriorTrivia(leadingTrivia) - .WithLeadingTrivia(SyntaxFactory.DocumentationCommentExterior("///")); - // Remove unnecessary nested paragraph elements contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes().OfType(), RemoveNestedParagraphs); @@ -193,8 +190,9 @@ private SyntaxNode RenderBlockElementAsMarkdown(SyntaxNode originalNode, SyntaxN return elementSyntax.WithContent( XmlSyntaxFactory.List( XmlSyntaxFactory.NewLine().WithoutTrailingTrivia(), - XmlSyntaxFactory.Text(rendered, true), - XmlSyntaxFactory.NewLine().WithoutTrailingTrivia())); + XmlSyntaxFactory.Text(" " + rendered.Replace("\n", "\n "), true), + XmlSyntaxFactory.NewLine().WithoutTrailingTrivia(), + XmlSyntaxFactory.Text(" "))); } private string RenderAsMarkdown(string text) From 55284a79fb8d4c5114d52d0a3f3100a5e06f53cc Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 6 Feb 2015 10:18:28 -0600 Subject: [PATCH 08/12] Fix handling of empty text tokens in WithoutFirstAndLastNewlines --- .../DocumentationSyntaxExtensions.cs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationSyntaxExtensions.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationSyntaxExtensions.cs index 81873d0..f56dd29 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationSyntaxExtensions.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/DocumentationSyntaxExtensions.cs @@ -172,14 +172,28 @@ public static SyntaxList WithoutFirstAndLastNewlines(this SyntaxL string trimmed = firstTokenText.TrimStart(); if (trimmed != firstTokenText) { - SyntaxToken newFirstToken = SyntaxFactory.Token( - firstTextToken.LeadingTrivia, - firstTextToken.CSharpKind(), - trimmed, - firstTextToken.ValueText.TrimStart(), - firstTextToken.TrailingTrivia); - - summaryContent = summaryContent.Replace(firstTextSyntax, firstTextSyntax.ReplaceToken(firstTextToken, newFirstToken)); + if (trimmed.Length == 0) + { + if (firstTextSyntax.TextTokens.Count == 1) + { + summaryContent = summaryContent.Remove(firstTextSyntax); + } + else + { + summaryContent = summaryContent.Replace(firstTextSyntax, firstTextSyntax.WithTextTokens(firstTextSyntax.TextTokens.RemoveAt(0))); + } + } + else + { + SyntaxToken newFirstToken = SyntaxFactory.Token( + firstTextToken.LeadingTrivia, + firstTextToken.CSharpKind(), + trimmed, + firstTextToken.ValueText.TrimStart(), + firstTextToken.TrailingTrivia); + + summaryContent = summaryContent.Replace(firstTextSyntax, firstTextSyntax.ReplaceToken(firstTextToken, newFirstToken)); + } } } } From 389d7c238afbf1ae28ba3af983e67ca3b1d94208 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 6 Feb 2015 10:26:22 -0600 Subject: [PATCH 09/12] Use a two-step process to remove unnecessary paragraphs --- .../RenderAsMarkdownCodeFix.cs | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index b20bff7..ff7a371 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -22,6 +22,9 @@ public class RenderAsMarkdownCodeFix : CodeFixProvider private static readonly ImmutableArray _fixableDiagnostics = ImmutableArray.Create(RenderAsMarkdownAnalyzer.DiagnosticId); + private static readonly SyntaxAnnotation UnnecessaryParagraphAnnotation = + new SyntaxAnnotation("OpenStack:UnnecessaryParagraph"); + public sealed override ImmutableArray GetFixableDiagnosticIds() { return _fixableDiagnostics; @@ -119,7 +122,8 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum return context.Document; // Remove unnecessary nested paragraph elements - contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes().OfType(), RemoveNestedParagraphs); + contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes().OfType(), MarkUnnecessaryParagraphs); + contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes().OfType(), RemoveUnnecessaryParagraphs); SyntaxNode root = await context.Document.GetSyntaxRootAsync(cancellationToken); SyntaxNode newRoot = root.ReplaceNode(documentationCommentTriviaSyntax, contentsOnly); @@ -129,13 +133,34 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum return context.Document.WithSyntaxRoot(newRoot); } - private SyntaxNode RemoveNestedParagraphs(SyntaxNode originalNode, SyntaxNode rewrittenNode) + private SyntaxNode MarkUnnecessaryParagraphs(SyntaxNode originalNode, SyntaxNode rewrittenNode) { XmlElementSyntax elementSyntax = rewrittenNode as XmlElementSyntax; if (!IsUnnecessaryParaElement(elementSyntax)) return rewrittenNode; - return elementSyntax.Content[0].WithTriviaFrom(rewrittenNode); + return elementSyntax.WithAdditionalAnnotations(UnnecessaryParagraphAnnotation); + } + + private SyntaxNode RemoveUnnecessaryParagraphs(XmlElementSyntax originalNode, XmlElementSyntax rewrittenNode) + { + bool hasUnnecessary = false; + SyntaxList content = rewrittenNode.Content; + for (int i = 0; i < content.Count; i++) + { + if (content[i].HasAnnotation(UnnecessaryParagraphAnnotation)) + { + hasUnnecessary = true; + XmlElementSyntax unnecessaryElement = (XmlElementSyntax)content[i]; + content = content.ReplaceRange(unnecessaryElement, unnecessaryElement.Content); + i += unnecessaryElement.Content.Count; + } + } + + if (!hasUnnecessary) + return rewrittenNode; + + return rewrittenNode.WithContent(content); } private static bool IsUnnecessaryParaElement(XmlElementSyntax elementSyntax) From fd78dd1df49b06d7acf18e54ac1e9fd597f78d30 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 6 Feb 2015 10:26:59 -0600 Subject: [PATCH 10/12] "Not equivalent" nodes which have the same "ToFullString()" value will crash the IDE --- .../OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index ff7a371..cc38a54 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -130,6 +130,9 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum if (documentationCommentTriviaSyntax.IsEquivalentTo(contentsOnly)) return context.Document; + if (documentationCommentTriviaSyntax.ToFullString().Equals(contentsOnly.ToFullString(), StringComparison.Ordinal)) + return context.Document; + return context.Document.WithSyntaxRoot(newRoot); } From ccaab4991684554374bbc9eb5869e5ba78c6a9c7 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 6 Feb 2015 10:28:29 -0600 Subject: [PATCH 11/12] Relax rules for identifying unnecessary paragraphs --- .../RenderAsMarkdownCodeFix.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index cc38a54..425da45 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -177,15 +177,30 @@ private static bool IsUnnecessaryParaElement(XmlElementSyntax elementSyntax) if (!IsParaElement(elementSyntax)) return false; - if (elementSyntax.Content.Count != 1) - return false; - - if (!IsParaElement(elementSyntax.Content[0] as XmlElementSyntax)) + if (HasLooseContent(elementSyntax.Content)) return false; return true; } + private static bool HasLooseContent(SyntaxList content) + { + foreach (XmlNodeSyntax node in content) + { + XmlTextSyntax textSyntax = node as XmlTextSyntax; + if (textSyntax != null) + { + if (textSyntax.TextTokens.Any(token => !string.IsNullOrWhiteSpace(token.ValueText))) + return true; + } + + if (node is XmlCDataSectionSyntax) + return true; + } + + return false; + } + private static bool HasAttributes(XmlElementSyntax syntax) { return syntax.StartTag?.Attributes.Count > 0; From 44eac368586f8e1efc43091e4113ca55e1782034 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 6 Feb 2015 10:28:55 -0600 Subject: [PATCH 12/12] Remove unnecessary para tags which appear as the single element of a element --- .../RenderAsMarkdownCodeFix.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs index 425da45..762c35b 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -139,10 +139,22 @@ private async Task CreateChangedDocument(CodeFixContext context, Docum private SyntaxNode MarkUnnecessaryParagraphs(SyntaxNode originalNode, SyntaxNode rewrittenNode) { XmlElementSyntax elementSyntax = rewrittenNode as XmlElementSyntax; - if (!IsUnnecessaryParaElement(elementSyntax)) - return rewrittenNode; + if (IsUnnecessaryParaElement(elementSyntax)) + return elementSyntax.WithAdditionalAnnotations(UnnecessaryParagraphAnnotation); + + if (string.Equals("summary", elementSyntax?.StartTag?.Name?.ToString(), StringComparison.Ordinal)) + { + SyntaxList trimmedContent = elementSyntax.Content.WithoutFirstAndLastNewlines(); + if (trimmedContent.Count == 1 + && IsParaElement(trimmedContent[0] as XmlElementSyntax) + && !HasAttributes(trimmedContent[0] as XmlElementSyntax)) + { + XmlNodeSyntax paraToRemove = elementSyntax.Content.GetFirstXmlElement("para"); + return elementSyntax.ReplaceNode(paraToRemove, paraToRemove.WithAdditionalAnnotations(UnnecessaryParagraphAnnotation)); + } + } - return elementSyntax.WithAdditionalAnnotations(UnnecessaryParagraphAnnotation); + return rewrittenNode; } private SyntaxNode RemoveUnnecessaryParagraphs(XmlElementSyntax originalNode, XmlElementSyntax rewrittenNode) @@ -203,7 +215,7 @@ private static bool HasLooseContent(SyntaxList content) private static bool HasAttributes(XmlElementSyntax syntax) { - return syntax.StartTag?.Attributes.Count > 0; + return syntax?.StartTag?.Attributes.Count > 0; } private static bool IsParaElement(XmlElementSyntax syntax)