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..f56dd29 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() + " "); @@ -161,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)); + } } } } 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..ad5cda5 --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownAnalyzer.cs @@ -0,0 +1,63 @@ +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.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) + { + 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..762c35b --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/RenderAsMarkdownCodeFix.cs @@ -0,0 +1,278 @@ +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); + + private static readonly SyntaxAnnotation UnnecessaryParagraphAnnotation = + new SyntaxAnnotation("OpenStack:UnnecessaryParagraph"); + + 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.ChildNodes(), 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().WithTrailingTrivia(SyntaxFactory.DocumentationCommentExterior("///"))); + + newContent = newContent.Add(XmlSyntaxFactory.Text(line.TrimEnd(), true)); + } + + contentsOnly = contentsOnly.WithContent(newContent); + contentsOnly = + contentsOnly + .ReplaceExteriorTrivia(leadingTrivia) + .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; + + // Remove unnecessary nested paragraph elements + 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); + if (documentationCommentTriviaSyntax.IsEquivalentTo(contentsOnly)) + return context.Document; + + if (documentationCommentTriviaSyntax.ToFullString().Equals(contentsOnly.ToFullString(), StringComparison.Ordinal)) + return context.Document; + + return context.Document.WithSyntaxRoot(newRoot); + } + + private SyntaxNode MarkUnnecessaryParagraphs(SyntaxNode originalNode, SyntaxNode rewrittenNode) + { + XmlElementSyntax elementSyntax = rewrittenNode as XmlElementSyntax; + 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 rewrittenNode; + } + + 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) + { + if (elementSyntax == null) + return false; + + if (HasAttributes(elementSyntax)) + return false; + + if (!IsParaElement(elementSyntax)) + return false; + + 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; + } + + 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; + 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.Replace("\n", "\n "), true), + XmlSyntaxFactory.NewLine().WithoutTrailingTrivia(), + XmlSyntaxFactory.Text(" "))); + } + + 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 @@  +