diff --git a/src/dotnet/APIView/APIView/Analysis/Analyzer.cs b/src/dotnet/APIView/APIView/Analysis/Analyzer.cs index 3d841036717..8a7f3f160be 100644 --- a/src/dotnet/APIView/APIView/Analysis/Analyzer.cs +++ b/src/dotnet/APIView/APIView/Analysis/Analyzer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using ApiView; @@ -9,7 +9,7 @@ namespace APIView.Analysis { - internal class Analyzer : SymbolVisitor + public class Analyzer : SymbolVisitor { public List Results { get; } = new List(); diff --git a/src/dotnet/APIView/APIView/CodeFileTokensBuilder.cs b/src/dotnet/APIView/APIView/CodeFileTokensBuilder.cs index e6d3617d435..e00fecc2951 100644 --- a/src/dotnet/APIView/APIView/CodeFileTokensBuilder.cs +++ b/src/dotnet/APIView/APIView/CodeFileTokensBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using APIView; @@ -78,4 +78,4 @@ public void Comment(string text) Append(text, CodeFileTokenKind.Comment); } } -} \ No newline at end of file +} diff --git a/src/dotnet/APIView/APIView/Languages/CodeFileBuilderSymbolSorter.cs b/src/dotnet/APIView/APIView/Languages/CodeFileBuilderSymbolSorter.cs index f873ecf4698..d31dc55359f 100644 --- a/src/dotnet/APIView/APIView/Languages/CodeFileBuilderSymbolSorter.cs +++ b/src/dotnet/APIView/APIView/Languages/CodeFileBuilderSymbolSorter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Microsoft.CodeAnalysis; @@ -8,7 +8,7 @@ namespace ApiView { - class CodeFileBuilderSymbolOrderProvider : ICodeFileBuilderSymbolOrderProvider + public class CodeFileBuilderSymbolOrderProvider : ICodeFileBuilderSymbolOrderProvider { public IEnumerable OrderTypes(IEnumerable symbols) where T : ITypeSymbol { @@ -92,4 +92,4 @@ private static int GetMemberOrder(ISymbol symbol) } } } -} \ No newline at end of file +} diff --git a/src/dotnet/APIView/APIView/Model/CodeDiagnostic.cs b/src/dotnet/APIView/APIView/Model/CodeDiagnostic.cs index 08b97df7445..27c6d9eae7a 100644 --- a/src/dotnet/APIView/APIView/Model/CodeDiagnostic.cs +++ b/src/dotnet/APIView/APIView/Model/CodeDiagnostic.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace APIView @@ -17,15 +17,10 @@ public CodeDiagnostic(string diagnosticId, string targetId, string text, string HelpLinkUri = helpLinkUri; Level = level; } - public string DiagnosticId { get; set; } - public string Text { get; set; } - public string HelpLinkUri { get; set; } - public string TargetId { get; set; } - public CodeDiagnosticLevel Level { get; set; } } } diff --git a/src/dotnet/APIView/APIView/Model/CodeFile.cs b/src/dotnet/APIView/APIView/Model/CodeFile.cs index 7de36caea3c..ee83a06bdef 100644 --- a/src/dotnet/APIView/APIView/Model/CodeFile.cs +++ b/src/dotnet/APIView/APIView/Model/CodeFile.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using APIView; +using APIView.TreeToken; using System; using System.Collections.Generic; using System.IO; @@ -20,12 +21,9 @@ public class CodeFile }; private string _versionString; - private static HashSet _collapsibleLanguages = new HashSet(new string[] { "Swagger" }); - [Obsolete("This is only for back compat, VersionString should be used")] public int Version { get; set; } - public string VersionString { #pragma warning disable 618 @@ -33,38 +31,24 @@ public string VersionString #pragma warning restore 618 set => _versionString = value; } - public string Name { get; set; } - public string Language { get; set; } - public string LanguageVariant { get; set; } - public string PackageName { get; set; } - public string ServiceName { get; set; } - public string PackageDisplayName { get; set; } - public string PackageVersion { get; set; } - public string CrossLanguagePackageId { get; set; } - public CodeFileToken[] Tokens { get; set; } = Array.Empty(); - + public List APIForest { get; set; } = new List(); public List LeafSections { get; set; } - public NavigationItem[] Navigation { get; set; } - public CodeDiagnostic[] Diagnostics { get; set; } - public override string ToString() { return new CodeFileRenderer().Render(this).CodeLines.ToString(); - } - + } public static bool IsCollapsibleSectionSSupported(string language) => _collapsibleLanguages.Contains(language); - public static async Task DeserializeAsync(Stream stream, bool hasSections = false) { var codeFile = await JsonSerializer.DeserializeAsync( diff --git a/src/dotnet/APIView/APIView/Model/StructuredTokenModel.cs b/src/dotnet/APIView/APIView/Model/StructuredTokenModel.cs new file mode 100644 index 00000000000..437db04f4da --- /dev/null +++ b/src/dotnet/APIView/APIView/Model/StructuredTokenModel.cs @@ -0,0 +1,294 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Text.Json.Serialization; + +namespace APIView.TreeToken +{ + + /// + /// Represents the type of a structured token. + /// All tokens should be content except for spacing tokens and url. + /// + public enum StructuredTokenKind + { + /// + /// Specifies that the token is content. + /// This is the default value for a token. + /// + Content = 0, + /// + /// Space token indicating switch to new line. + /// + LineBreak = 1, + /// + /// Regular single space. + /// + NonBreakingSpace = 2, + /// + /// 4 NonBreakingSpaces. + /// + TabSpace = 3, + /// + /// Use this between method parameters. Depending on user setting this would result in a single space or new line. + /// + ParameterSeparator = 4 + } + + /// + /// Represents an APIView token its properties and tags for APIView parsers. + /// + + public class StructuredToken + { + /// + /// Property key to indicate that a range of tokens is a group + /// + public static string GROUP_ID = "GroupId"; + /// + /// Property key to indicate id to be navigated to when a token is clicked. + /// + public static string NAVIGATE_TO_ID = "NavigateToId"; + /// + /// Property value that marks a token as documentation + /// + public static string DOCUMENTATION = "doc"; + /// + /// Style class for text + /// + public static string TEXT = "text"; + /// + /// Style class for keyword + /// + public static string KEYWORD = "keyword"; + /// + /// Style class for literal + /// + public static string LITERAL = "literal"; + /// + /// Style class for member-name + /// + public static string MEMBER_NAME = "mname"; + /// + /// Style class for punctuation + /// + public static string PUNCTUATION = "punc"; + /// + /// Style class for string-literal + /// + public static string STRING_LITERAL = "sliteral"; + /// + /// Style class for type-name + /// + public static string TYPE_NAME = "tname"; + /// + /// Style class for comment + /// + public static string COMMENT = "comment"; + + + /// + /// Token Id. + /// Previously known as DefinitionId. + /// + public string Id { get; set; } + + /// + /// Represents the type of a structured token. + /// + public StructuredTokenKind Kind { get; set; } = StructuredTokenKind.Content; + + /// + /// The token value which will be displayed. Spacing tokens don't need to have value as it will be + /// replaced at deserialization based on the Token Kind. + /// + public string Value { get; set; } = string.Empty; + + /// + /// Properties of the token. + /// + /// + /// GroupId: `doc` to group consecutive comment tokens as documentation. + /// + /// + /// NavigateToId: When the token is clicked, will navigate to the location that matches the provided token id. + /// + /// + /// + public Dictionary Properties + { + get { return PropertiesObj.Count > 0 ? PropertiesObj : null; } + set { PropertiesObj = value ?? new Dictionary(); } + } + + /// + /// List of default CSS configuration for any language. + /// Languages can override these or add new classes by reaching out to the APIView team. + /// + /// + /// comment + /// + /// + /// keyword + /// + /// + /// literal + /// + /// + /// mname: member name + /// + /// + /// punc + /// + /// + /// sliteral: string literal + /// + /// + /// text + /// + /// + /// tname: type name + /// + /// + /// + public HashSet RenderClasses + { + get { return RenderClassesObj.Count > 0 ? RenderClassesObj : null; } + set { RenderClassesObj = value ?? new HashSet(); } + } + + /// + /// Behavioral boolean properties + /// + /// + /// Deprecated: Mark a node as deprecated + /// + /// + /// SkipDiff: Indicate that a node should not be used in computation of diff. + /// + /// + /// + public HashSet Tags + { + get { return TagsObj.Count > 0 ? TagsObj : null; } + set { TagsObj = value ?? new HashSet(); } + } + + [JsonIgnore] + public HashSet TagsObj { get; set; } = new HashSet(); + + [JsonIgnore] + public Dictionary PropertiesObj { get; set; } = new Dictionary(); + + [JsonIgnore] + public HashSet RenderClassesObj { get; set; } = new HashSet(); + + public StructuredToken() + { + new StructuredToken(string.Empty); + } + + public StructuredToken(string value) + { + Value = value; + Kind = StructuredTokenKind.Content; + } + + public static StructuredToken CreateLineBreakToken() + { + var token = new StructuredToken(); + token.Kind = StructuredTokenKind.LineBreak; + return token; + } + + public static StructuredToken CreateEmptyToken(string id = null) + { + var token = new StructuredToken(); + token.Id = id; + token.Kind = StructuredTokenKind.Content; + return token; + } + + public static StructuredToken CreateSpaceToken() + { + var token = new StructuredToken(); + token.Kind = StructuredTokenKind.NonBreakingSpace; + return token; + } + + public static StructuredToken CreateParameterSeparatorToken() + { + var token = new StructuredToken(); + token.Kind = StructuredTokenKind.ParameterSeparator; + return token; + } + + public static StructuredToken CreateTextToken(string value, string id = null) + { + var token = new StructuredToken(value); + if (!string.IsNullOrEmpty(id)) + { + token.Id = id; + } + token.RenderClassesObj.Add(TEXT); + return token; + } + + public static StructuredToken CreateKeywordToken(string value) + { + var token = new StructuredToken(value); + token.RenderClassesObj.Add(KEYWORD); + return token; + } + + public static StructuredToken CreateKeywordToken(SyntaxKind syntaxKind) + { + return CreateKeywordToken(SyntaxFacts.GetText(syntaxKind)); + } + + public static StructuredToken CreateKeywordToken(Accessibility accessibility) + { + return CreateKeywordToken(SyntaxFacts.GetText(accessibility)); + } + + public static StructuredToken CreatePunctuationToken(string value) + { + var token = new StructuredToken(value); + token.RenderClassesObj.Add(PUNCTUATION); + return token; + } + + public static StructuredToken CreatePunctuationToken(SyntaxKind syntaxKind) + { + return CreatePunctuationToken(SyntaxFacts.GetText(syntaxKind)); + } + + public static StructuredToken CreateTypeNameToken(string value) + { + var token = new StructuredToken(value); + token.RenderClassesObj.Add(TYPE_NAME); + return token; + } + + public static StructuredToken CreateMemberNameToken(string value) + { + var token = new StructuredToken(value); + token.RenderClassesObj.Add(MEMBER_NAME); + return token; + } + + public static StructuredToken CreateLiteralToken(string value) + { + var token = new StructuredToken(value); + token.RenderClassesObj.Add(LITERAL); + return token; + } + + public static StructuredToken CreateStringLiteralToken(string value) + { + var token = new StructuredToken(value); + token.RenderClassesObj.Add(STRING_LITERAL); + return token; + } + } +} diff --git a/src/dotnet/APIView/APIView/Model/TokenTreeModel.cs b/src/dotnet/APIView/APIView/Model/TokenTreeModel.cs new file mode 100644 index 00000000000..f6ff4043519 --- /dev/null +++ b/src/dotnet/APIView/APIView/Model/TokenTreeModel.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace APIView.TreeToken +{ + public class APITreeNode + { + /// + /// Indicates a node is an assembly kind + /// + public static string ASSEMBLY = "Assembly"; + /// + /// Indicates a node is a InternalsVisibleTo kind + /// + public static string INTERNALS_VISIBLE_TO = "InternalsVisibleTo"; + /// + /// Indicates a node is a Dependencies kind + /// + public static string DEPENDENCIES = "Dependencies"; + /// + /// Indicates a node is a Namespace kind + /// + public static string NAMESPACE = "Namespace"; + /// + /// Indicates a node is a Type kind + /// + public static string TYPE = "Type"; + /// + /// Indicates a node is a Member kind + /// + public static string MEMBER = "Member"; + /// + /// Indicates a node is hidden + /// + public static string HIDDEN = "Hidden"; + /// + /// Indicates that a node should be hidden from Navigation + /// + public static string HIDE_FROM_NAV = "HideFromNav"; + /// + /// Property key to use to make a nodes kind more specific. + /// + public static string SUB_KIND = "SubKind"; + + /// + /// Id of the node, which should be unique at the node level. i.e. unique among its siblings. + /// This was previously represented by the DefinitionId for the main Token of the node. + /// + public string Id { get; set; } + + /// + /// The type of node it is. Using any of the following `assembly`, `class`, `delegate`, `enum`, `interface`, + /// `method` , `namespace`, `package`, `struct`, `type` will get you the corresponding default icons + /// for the page navigation. + /// + public string Kind { get; set; } + + /// + /// The name of the tree node which will be used as label for the API Navigation. + /// Generally use the name of the module (class, method). + /// + public string Name { get; set; } + + /// + /// Tokens which are rendered after all child nodes. + /// Depending on the language this would include the closing curly brace and/or empty lines. + /// You can simulate an empty line by adding an empty token (Content token with no value) and a LineBreak token. + /// + public List BottomTokens + { + get { return BottomTokensObj.Count > 0 ? BottomTokensObj : null; } + set { BottomTokensObj = value ?? new List(); } + } + /// + /// The nodes immediate children. + /// For a namespace this would be classes, for a class this would be the class constructors and methods. + /// Children are rendered after TopTokens but before BottomTokens, and are automatically indented. + /// + public List Children + { + get { return ChildrenObj.Count > 0 ? ChildrenObj : null; } + set { ChildrenObj = value ?? new List(); } + } + /// + /// Properties of a node. + /// + /// + /// SubKind: Similar to kind, use this to make the node more specific. + /// e.g. `Kind = 'Type'`, and `SubKind = 'class'`. We also use this to make the navigation icon it will override kind. + /// + /// + /// IconName: Use this only if you are looking to add a custom icon different from language wide defaults. New additions will need to be supported APIView side. + /// + /// + /// + public Dictionary Properties + { + get { return PropertiesObj.Count > 0 ? PropertiesObj : null; } + set { PropertiesObj = value ?? new Dictionary(); } + } + + /// + /// Behavioral boolean properties. + /// + /// + /// Deprecated: Mark a node as deprecated + /// + /// + /// Hidden: Mark a node as Hidden + /// + /// + /// HideFromNavigation: Indicate that a node should be hidden from the page navigation. + /// + /// + /// SkipDiff: Indicate that a node should not be used in computation of diff. + /// + /// + /// CrossLangDefId: The cross language definitionId for the node. + /// + /// + /// + public HashSet Tags + { + get { return TagsObj.Count > 0 ? TagsObj : null; } + set { TagsObj = value ?? new HashSet(); } + } + + /// + /// The main data of the node. This is all the tokens that actually define the node. + /// When rendering, TopTokens are rendered first, followed by any Children, and then finally BottomTokens + /// + public List TopTokens + { + get { return TopTokensObj.Count > 0 ? TopTokensObj : null; } + set { TopTokensObj = value ?? new List(); } + } + + [JsonIgnore] + public HashSet TagsObj { get; set; } = new HashSet(); + + [JsonIgnore] + public Dictionary PropertiesObj { get; set; } = new Dictionary(); + + [JsonIgnore] + public List TopTokensObj { get; set; } = new List(); + + [JsonIgnore] + public List BottomTokensObj { get; set; } = new List(); + + [JsonIgnore] + public List ChildrenObj { get; set; } = new List(); + + public override int GetHashCode() + { + int hash = 17; + hash = hash * 23 + (Name != null ? Name.GetHashCode() : 0); + hash = hash * 23 + (Id != null ? Id.GetHashCode() : 0); + hash = hash * 23 + (Kind != null ? Kind.GetHashCode() : 0); + return hash; + } + + public override bool Equals(object obj) + { + var other = obj as APITreeNode; + if (other == null) + { + return false; + } + return Name == other.Name && Id == other.Id && Kind == other.Kind; + } + + public void SortChildren() + { + if (ChildrenObj != null) + { + if (Kind.Equals("Namespace") || Kind.Equals("Type") || Kind.Equals("Member")) + { + ChildrenObj.Sort((x, y) => x.Name.CompareTo(y.Name)); + } + foreach (var child in ChildrenObj) + { + child.SortChildren(); + } + } + } + } +} diff --git a/src/dotnet/APIView/APIView/SymbolDisplay/AbstractSymbolDisplayVisitor.cs b/src/dotnet/APIView/APIView/SymbolDisplay/AbstractSymbolDisplayVisitor.cs index 647ee35fd7c..2ab7a53af06 100644 --- a/src/dotnet/APIView/APIView/SymbolDisplay/AbstractSymbolDisplayVisitor.cs +++ b/src/dotnet/APIView/APIView/SymbolDisplay/AbstractSymbolDisplayVisitor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; @@ -7,9 +7,9 @@ namespace Microsoft.CodeAnalysis.SymbolDisplay { - internal abstract partial class AbstractSymbolDisplayVisitor : SymbolVisitor + public abstract partial class AbstractSymbolDisplayVisitor : SymbolVisitor { - protected readonly ArrayBuilder builder; + public readonly ArrayBuilder builder; protected readonly SymbolDisplayFormat format; protected readonly bool isFirstSymbolVisited; protected readonly bool inNamespaceOrType; @@ -20,7 +20,7 @@ internal abstract partial class AbstractSymbolDisplayVisitor : SymbolVisitor private AbstractSymbolDisplayVisitor _lazyNotFirstVisitor; private AbstractSymbolDisplayVisitor _lazyNotFirstVisitorNamespaceOrType; - protected AbstractSymbolDisplayVisitor( + public AbstractSymbolDisplayVisitor( ArrayBuilder builder, SymbolDisplayFormat format, bool isFirstSymbolVisited, diff --git a/src/dotnet/APIView/APIView/SymbolDisplay/ArrayBuilder.Enumerator.cs b/src/dotnet/APIView/APIView/SymbolDisplay/ArrayBuilder.Enumerator.cs index aa4fe5eda7e..8862d0ebb16 100644 --- a/src/dotnet/APIView/APIView/SymbolDisplay/ArrayBuilder.Enumerator.cs +++ b/src/dotnet/APIView/APIView/SymbolDisplay/ArrayBuilder.Enumerator.cs @@ -1,15 +1,15 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; namespace Microsoft.CodeAnalysis.PooledObjects { - internal partial class ArrayBuilder + public partial class ArrayBuilder { /// /// struct enumerator used in foreach. /// - internal struct Enumerator : IEnumerator + public struct Enumerator : IEnumerator { private readonly ArrayBuilder _builder; private int _index; diff --git a/src/dotnet/APIView/APIView/SymbolDisplay/ArrayBuilder.cs b/src/dotnet/APIView/APIView/SymbolDisplay/ArrayBuilder.cs index d915ed00eb8..da8e0b0c2f6 100644 --- a/src/dotnet/APIView/APIView/SymbolDisplay/ArrayBuilder.cs +++ b/src/dotnet/APIView/APIView/SymbolDisplay/ArrayBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; @@ -9,7 +9,7 @@ namespace Microsoft.CodeAnalysis.PooledObjects { [DebuggerDisplay("Count = {Count,nq}")] [DebuggerTypeProxy(typeof(ArrayBuilder<>.DebuggerProxy))] - internal sealed partial class ArrayBuilder : IReadOnlyCollection, IReadOnlyList + public sealed partial class ArrayBuilder : IReadOnlyCollection, IReadOnlyList { #region DebuggerProxy diff --git a/src/dotnet/APIView/APIView/SymbolDisplay/ObjectDisplay.cs b/src/dotnet/APIView/APIView/SymbolDisplay/ObjectDisplay.cs index 4bf7aa37221..f94426fd21b 100644 --- a/src/dotnet/APIView/APIView/SymbolDisplay/ObjectDisplay.cs +++ b/src/dotnet/APIView/APIView/SymbolDisplay/ObjectDisplay.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Globalization; @@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.CSharp /// /// #pragma warning restore CA1200 // Avoid using cref tags with a prefix - internal static class ObjectDisplay + public static class ObjectDisplay { /// /// Returns a string representation of an object of primitive type. diff --git a/src/dotnet/APIView/APIView/SymbolDisplay/ObjectDisplayOptions.cs b/src/dotnet/APIView/APIView/SymbolDisplay/ObjectDisplayOptions.cs index 08ff3839722..7daffcb5687 100644 --- a/src/dotnet/APIView/APIView/SymbolDisplay/ObjectDisplayOptions.cs +++ b/src/dotnet/APIView/APIView/SymbolDisplay/ObjectDisplayOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis /// Specifies the options for how generics are displayed in the description of a symbol. /// [Flags] - internal enum ObjectDisplayOptions + public enum ObjectDisplayOptions { /// /// Format object using default options. diff --git a/src/dotnet/APIView/APIView/SymbolDisplay/ObjectPool`1.cs b/src/dotnet/APIView/APIView/SymbolDisplay/ObjectPool`1.cs index 2f03191a227..04793d4720e 100644 --- a/src/dotnet/APIView/APIView/SymbolDisplay/ObjectPool`1.cs +++ b/src/dotnet/APIView/APIView/SymbolDisplay/ObjectPool`1.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // define TRACE_LEAKS to get additional diagnostics that can lead to the leak sources. note: it will // make everything about 2-3x slower @@ -37,7 +37,7 @@ namespace Microsoft.CodeAnalysis.PooledObjects /// Rationale: /// If there is no intent for reusing the object, do not use pool - just use "new". /// - internal class ObjectPool where T : class + public class ObjectPool where T : class { [DebuggerDisplay("{Value,nq}")] private struct Element diff --git a/tools/apiview/parsers/CONTRIBUTING.md b/tools/apiview/parsers/CONTRIBUTING.md new file mode 100644 index 00000000000..b5b3c31b52f --- /dev/null +++ b/tools/apiview/parsers/CONTRIBUTING.md @@ -0,0 +1,1166 @@ +# Tree token parser + +This page describes how to contribute to [APIView](../../../src//dotnet/APIView/APIViewWeb/APIViewWeb.csproj) language level parsers. +Specifically how to create or update a language parser to produce a hierarchy of the API using a tree data tokens, instead of a flat token list. + +## Tree style token parser benefits + +- Ability to granularly identify a specific class and it’s methods or a specific API alone within a large review without rendering entire tokens. +- Faster diffing using tree shaker instead of current text based comparison. +- Provide diffing with context of where the line that changed belong to in the tree, instead of showing the 5 lines before and after the change. +- Provide cross language view for a granular section within an API review. +- Support for collapsible views at each class and namespace level to make it easier to review the change. User will be able to expand/collapse an entire class or a namespace and it’s children. +- Support dynamic representation of APIs for e.g. some users prefer to see all method arguments in same line and some users prefer to see them in multiple lines. +- Less data to be stored in token file which are located in azure storage blob. + +## Key concepts + +Previously APIview tokens were created as a flat list assigned to the `CodeFileToken[] Tokens` property of the [CodeFile](../../../src/dotnet/APIView/APIView/Model/CodeFile.cs). Then the page navigation is created and assigned to `NavigationItem[] Navigation`. For tree style tokens these two properties are no longer required, instead a `List APIForest` property will be used to capture the generated tree of tokens. + +![APITree](images/APITree.svg) + +- Each module of the API (namespace, class, methods) should be its own node. Members of a module (methods in a class), (classes in a namespace) should be added as child nodes of its parent module. +- Each tree node has top tokens which should be used to capture the main tokens on the node, these can span multiple lines. +- Module name, decorators, and parameters should be modeled as `TopTokens`. +- If the language requires it use the bottom tokens to capture tokens that closes out the node, this is usually just the closing bracket and/or empty lines. + +### Object Definitions + +- Here are the models needed + + ``` + object APITreeNode + string Name (Required) + string Id (Required) + string Kind (Required) + Set Tags + Dictionary Properties + List TopTokens + List BottomTokens + List Children + + object StructuredToken + string Value + string Id + StructuredTokenKind Kind (Required) + Set Tags + Dictionary Properties + Set RenderClasses + + enum StructuredTokenKind + Content + LineBreak + NonBreakingSpace + TabSpace + ParameterSeparator + ``` + +### [APITreeNode](../../../src/dotnet/APIView/APIView/Model/TokenTreeModel.cs) +- Ensure each node has an Id and Kind. The combination of `Id`, `Kind`, and `SubKind` should make the node unique across all nodes in the tree. For example a class and a method can potentially have the same Id, but the kind should differentiate them from each other. +- Sort each node at each level of the tree by your desired property, this is to ensure that difference in node order does not result in diff. + +### [StructuredToken](../../../src/dotnet/APIView/APIView/Model/StructuredTokenModel.cs) + +- Assign the final parsed value to a `List APIForest` property of the `CodeFile` + +## Serialization + +Serialize the generated code file to JSON and then compress the file using Gzip compression. Try to make the json as small as possible by ignoring null values and empty collections. +Don't worry about indentation that will be handled by the tree structure. In the case you want to have indentation between the tokens then use `TabSpace` token kind. + +## Examples + +### APITreeNode and StructuredToken + +Example showing a full node with all its children and tokens. + +![Navigation](images/apitree-node-and-tokens.png) + + + +
+click to expand json + + ```json + { + "Id": "Azure.Storage.Blobs.Models.BlobContainerInfo", + "Kind": "Type", + "Name": "BlobContainerInfo", + "BottomTokens": [ + { + "Kind": 0, + "Value": "}", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "" + } + ], + "Children": [ + { + "Id": "Azure.Storage.Blobs.Models.BlobContainerInfo.ETag", + "Kind": "Member", + "Name": "Azure.Storage.Blobs.Models.BlobContainerInfo.ETag", + "Properties": { + "SubKind": "Property" + }, + "Tags": [ + "HideFromNav" + ], + "TopTokens": [ + { + "Kind": 0, + "Value": "// \u003Csummary\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// The ETag contains a value that you can use to perform operations conditionally. If the request version is 2011-08-18 or newer, the ETag value will be in quotes.", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// \u003C/summary\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Id": "Azure.Storage.Blobs.Models.BlobContainerInfo.ETag", + "Kind": 0, + "Value": "" + }, + { + "Kind": 0, + "Value": "public", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "ETag", + "RenderClasses": [ + "tname" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "ETag", + "RenderClasses": [ + "mname" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "{", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "get", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 0, + "Value": ";", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "}", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 1, + "Value": "" + } + ] + }, + { + "Id": "Azure.Storage.Blobs.Models.BlobContainerInfo.LastModified", + "Kind": "Member", + "Name": "Azure.Storage.Blobs.Models.BlobContainerInfo.LastModified", + "Properties": { + "SubKind": "Property" + }, + "Tags": [ + "HideFromNav" + ], + "TopTokens": [ + { + "Kind": 0, + "Value": "// \u003Csummary\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// Returns the date and time the container was last modified. Any operation that modifies the blob, including an update of the blob\u0027s metadata or properties, changes the last-modified time of the blob.", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// \u003C/summary\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Id": "Azure.Storage.Blobs.Models.BlobContainerInfo.LastModified", + "Kind": 0, + "Value": "" + }, + { + "Kind": 0, + "Value": "public", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "DateTimeOffset", + "RenderClasses": [ + "tname" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "LastModified", + "RenderClasses": [ + "mname" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "{", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "get", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 0, + "Value": ";", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "}", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 1, + "Value": "" + } + ] + } + ], + "Properties": { + "SubKind": "class" + }, + "TopTokens": [ + { + "Kind": 0, + "Value": "// \u003Csummary\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// BlobContainerInfo", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// \u003C/summary\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "public", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "class", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Id": "Azure.Storage.Blobs.Models.BlobContainerInfo", + "Kind": 0, + "Value": "" + }, + { + "Id": "Azure.Storage.Blobs.Models.BlobContainerInfo", + "Kind": 0, + "Value": "BlobContainerInfo", + "RenderClasses": [ + "tname" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "{", + "RenderClasses": [ + "punc" + ] + } + ] +} + ``` +
+ +### Navigation +![Navigation](images/navigation.png) + +Ensure you set appropriate `Name` for the token node. And also set a value for the `Kind`. You can use one of the following values `assembly`, `class`, `delegate`, `enum`, `interface`, `method` , `namespace`, `package`, `struct`, `type` to get the default navigation icon. You can also use values more appropriate for your language then reach out to APIView to provide support for those. Use tag `HideFromNav` to exclude a node from showing up in the page navigation. +
+click to expand json + + ```json + { + "Id": "Azure.Storage.Blobs.Models", + "Kind": "Namespace", + "Name": "Azure.Storage.Blobs.Models", + "Children": [ + { + "Id": "Azure.Storage.Blobs.Models.AccessTier", + "Kind": "Type", + "Name": "AccessTier", + "TopTokens": [], + "BottomTokens": [], + "Children": [ + { + "Id": "Azure.Storage.Blobs.Models.AccessTier.AccessTier(System.String)", + "Kind": "Member", + "Name": "Azure.Storage.Blobs.Models.AccessTier.AccessTier(string)", + "TopTokens": [], + "BottomTokens": [], + "Properties": { + "SubKind": "Method" + }, + "Tags": [ + "HideFromNav" + ] + } + ], + "Properties": { + "SubKind": "struct" + } + }, + { + "Id": "Azure.Storage.Blobs.Models.AccountInfo", + "Kind": "Type", + "Name": "AccountInfo" + } + ] + } + ``` +
+ +### Documentation +![Navigation](images/documentation.png) + +You can group a sequence of similar type tokens using GroupId. Set `GroupId` property to `doc` to mark a sequence of comment tokens as documentation. +
+click to expand json + + ```json + { + "Id": "Azure.Storage.Blobs.BlobClient.BlobClient()", + "Kind": "Member", + "Name": "Azure.Storage.Blobs.BlobClient.BlobClient()", + "Properties": { + "SubKind": "Method" + }, + "Tags": [ + "HideFromNav" + ], + "TopTokens": [ + { + "Kind": 0, + "Value": "// \u003Csummary\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// Initializes a new instance of the \u003Csee cref=\u0022T:Azure.Storage.Blobs.BlobClient\u0022 /\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// class for mocking.", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + }, + { + "Kind": 1, + "Value": "" + }, + { + "Kind": 0, + "Value": "// \u003C/summary\u003E", + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ] + } + ] + } + ``` +
+ +### Deprecated Node +![Navigation](images/deprecated-node.png) + +Add `Deprecated` tag to a node to mark all the token of the node as deprecated. +
+click to expand json + + ```json + { + "Name": "Azure.Template.Models.SecretBundle.ContentType", + "Id": "Azure.Template.Models.SecretBundle.ContentType", + "Kind": "Member", + "Tags": [ + "HideFromNav", + "Deprecated" + ], + "Properties": { + "SubKind": "Property" + }, + "TopTokens": [ + { + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ], + "Value": "// \u003Csummary\u003E The content type of the secret. \u003C/summary\u003E", + "Kind": 0 + }, + { + "Value": "", + "Kind": 1 + }, + { + "Value": "", + "Id": "Azure.Template.Models.SecretBundle.ContentType", + "Kind": 0 + }, + { + "RenderClasses": [ + "keyword" + ], + "Value": "public", + "Kind": 0 + }, + { + "Value": "", + "Kind": 2 + }, + { + "RenderClasses": [ + "keyword" + ], + "Value": "string", + "Kind": 0 + }, + { + "Value": "", + "Kind": 2 + }, + { + "RenderClasses": [ + "mname" + ], + "Value": "ContentType", + "Kind": 0 + }, + { + "Value": "", + "Kind": 2 + }, + { + "RenderClasses": [ + "punc" + ], + "Value": "{", + "Kind": 0 + }, + { + "Value": "", + "Kind": 2 + }, + { + "RenderClasses": [ + "keyword" + ], + "Value": "get", + "Kind": 0 + }, + { + "RenderClasses": [ + "punc" + ], + "Value": ";", + "Kind": 0 + }, + { + "Value": "", + "Kind": 2 + }, + { + "RenderClasses": [ + "punc" + ], + "Value": "}", + "Kind": 0 + }, + { + "Value": "", + "Kind": 1 + } + ] +}, + ``` +
+ +### Deprecated Token +![Navigation](images/deprecated-token.png) + +Add `Deprecated` tag to a token to mark the token as deprecated. +
+click to expand json + + ```json + { + "Tags": [ + "Deprecated" + ], + "RenderClasses": [ + "text" + ], + "Value": "cancellationToken", + "Kind": 0 + } + ``` +
+ +### Hidden API +![Navigation](images/hidden-api.png) + +Add `Hidden` tag to a node to mark the node as hidden. Hidden nodes show up with low contrast in APIView. Hidden node visibility can also be toggled on or off in page settings. +
+click to expand json + + ```json + { + "Id": "Azure.Storage.Blobs.BlobServiceClient.UndeleteBlobContainer(System.String, System.String, System.String, System.Threading.CancellationToken)", + "Kind": "Member", + "Name": "Azure.Storage.Blobs.BlobServiceClient.UndeleteBlobContainer(string, string, string, System.Threading.CancellationToken)", + "Properties": { + "SubKind": "Method" + }, + "Tags": [ + "HideFromNav", + "Hidden" + ], + "TopTokens": [ + { + "Id": "Azure.Storage.Blobs.BlobServiceClient.UndeleteBlobContainer(System.String, System.String, System.String, System.Threading.CancellationToken)", + "Kind": 0, + "Value": "" + }, + { + "Kind": 0, + "Value": "public", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "virtual", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "Response", + "RenderClasses": [ + "tname" + ] + }, + { + "Kind": 0, + "Value": "\u003C", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 0, + "Value": "BlobContainerClient", + "Properties": { + "NavigateToId": "Azure.Storage.Blobs.BlobContainerClient" + }, + "RenderClasses": [ + "tname" + ] + }, + { + "Kind": 0, + "Value": "\u003E", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "UndeleteBlobContainer", + "RenderClasses": [ + "mname" + ] + }, + { + "Kind": 0, + "Value": "(", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 0, + "Value": "string", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "deletedContainerName", + "RenderClasses": [ + "text" + ] + }, + { + "Kind": 0, + "Value": ",", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 4, + "Value": "" + }, + { + "Kind": 0, + "Value": "string", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "deletedContainerVersion", + "RenderClasses": [ + "text" + ] + }, + { + "Kind": 0, + "Value": ",", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 4, + "Value": "" + }, + { + "Kind": 0, + "Value": "string", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "destinationContainerName", + "RenderClasses": [ + "text" + ] + }, + { + "Kind": 0, + "Value": ",", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 4, + "Value": "" + }, + { + "Kind": 0, + "Value": "CancellationToken", + "RenderClasses": [ + "tname" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "cancellationToken", + "RenderClasses": [ + "text" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "=", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "default", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 0, + "Value": ")", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 0, + "Value": ";", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 1, + "Value": "" + } + ] +} + ``` +
+ + + +### Parameter Separator +Use token with kind Kind `ParameterSeparator` between method parameters to allow APIView to either render the methods parameters on the same line or on separate lines based on language or user preference. + +![Navigation](images/parameter-separator-with-single-space.png) + +![Navigation](images/parameter-separator-with-line-break.png) + + +
+click to expand json + + ```json + [ + { + "Kind": 0, + "Value": "string", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "connectionString", + "RenderClasses": [ + "text" + ] + }, + { + "Kind": 0, + "Value": ",", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 4, + "Value": "" + }, + { + "Kind": 0, + "Value": "string", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "blobContainerName", + "RenderClasses": [ + "text" + ] + }, + { + "Kind": 0, + "Value": ",", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 4, + "Value": "" + }, + { + "Kind": 0, + "Value": "string", + "RenderClasses": [ + "keyword" + ] + }, + { + "Kind": 2, + "Value": "" + }, + { + "Kind": 0, + "Value": "blobName", + "RenderClasses": [ + "text" + ] + }, + { + "Kind": 0, + "Value": ")", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 0, + "Value": ";", + "RenderClasses": [ + "punc" + ] + }, + { + "Kind": 1, + "Value": "" + } + ] + ``` +
+ +### Navigation Token + +Make a token a navigation token by adding key `NavigateToId` to the token property with a value corresponding the the Id of the APINode to navigate to on click. +
+click to expand json + + ```json + { + "Kind": 0, + "Value": "BlobClientOptions", + "Properties": { + "NavigateToId": "Azure.Storage.Blobs.BlobClientOptions" + }, + "RenderClasses": [ + "tname" + ] + } + ``` +
+ +### URL Token +![Navigation](images/url-token.png) + +Make a token a Url by adding key `NavigateToUrl` to the token property and setting the value to the target Url. +
+click to expand json + + ```json + [ + { + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ], + "Value": "// This", + "Kind": 0 + }, + { + "Value": "", + "Kind": 2 + }, + { + "Properties": { + "GroupId": "doc", + "NavigateToUrl": "https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/ProtocolMethods.md" + }, + "RenderClasses": [ + "comment" + ], + "Value": "protocol method", + "Kind": 0 + }, + { + "Value": "", + "Kind": 2 + }, + { + "Properties": { + "GroupId": "doc" + }, + "RenderClasses": [ + "comment" + ], + "Value": "allows explicit creation of the request and processing of the response for advanced scenarios.", + "Kind": 0 + } + ] + ``` +
+ +## Get help + +Please reach out at [APIView Teams Channel](https://teams.microsoft.com/l/channel/19%3A3adeba4aa1164f1c889e148b1b3e3ddd%40thread.skype/APIView?groupId=3e17dcb0-4257-4a30-b843-77f47f1d4121&tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47) if you need more information. diff --git a/tools/apiview/parsers/csharp-api-parser/CSharpAPIParser.csproj b/tools/apiview/parsers/csharp-api-parser/CSharpAPIParser.csproj new file mode 100644 index 00000000000..914dbe16205 --- /dev/null +++ b/tools/apiview/parsers/csharp-api-parser/CSharpAPIParser.csproj @@ -0,0 +1,22 @@ + + + + Exe + true + CSharpAPIParserForAPIView + net7.0 + CSharpAPIParser + enable + enable + + + + + + + + + + + + diff --git a/tools/apiview/parsers/csharp-api-parser/Program.cs b/tools/apiview/parsers/csharp-api-parser/Program.cs new file mode 100644 index 00000000000..375bd05fd4b --- /dev/null +++ b/tools/apiview/parsers/csharp-api-parser/Program.cs @@ -0,0 +1,146 @@ +using System.CommandLine; +using System.IO.Compression; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Xml.Linq; +using ApiView; + +var inputOption = new Option("--packageFilePath", "C# Package (.nupkg) file").ExistingOnly(); +inputOption.IsRequired = true; + +var outputOption1 = new Option("--outputDirectoryPath", "Directory for the output Token File").ExistingOnly(); +var outputOption2 = new Option("--outputFileName", "Output File Name"); +var runAnalysis = new Argument("runAnalysis", "Run Analysis on the package"); +runAnalysis.SetDefaultValue(true); + +var rootCommand = new RootCommand("Parse C# Package (.nupkg) to APIView Tokens") +{ + inputOption, + outputOption1, + outputOption2, + runAnalysis +}; + +rootCommand.SetHandler((FileInfo packageFilePath, DirectoryInfo outputDirectory, string outputFileName, bool runAnalysis) => +{ + try + { + using (var stream = packageFilePath.OpenRead()) + { + HandlePackageFileParsing(stream, packageFilePath, outputDirectory, outputFileName, runAnalysis); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error Reading PackageFile : {ex.Message}"); + } +}, inputOption, outputOption1,outputOption2, runAnalysis); + +return rootCommand.InvokeAsync(args).Result; + + +static void HandlePackageFileParsing(Stream stream, FileInfo packageFilePath, DirectoryInfo OutputDirectory, string outputFileName, bool runAnalysis) +{ + ZipArchive? zipArchive = null; + Stream? dllStream = stream; + Stream? docStream = null; + List? dependencies = null; + + try + { + if (IsNuget(packageFilePath.FullName)) + { + zipArchive = new ZipArchive(stream, ZipArchiveMode.Read); + var nuspecEntry = zipArchive.Entries.Single(entry => IsNuspec(entry.Name)); + var dllEntries = zipArchive.Entries.Where(entry => IsDll(entry.Name)).ToArray(); + + if (dllEntries.Length == 0) + { + Console.Error.WriteLine($"PackageFile {packageFilePath.FullName} contains no dlls."); + return; + } + + var dllEntry = dllEntries.First(); + if (dllEntries.Length > 1) + { + // If there are multiple dlls in the nupkg (e.g. Cosmos), try to find the first that matches the nuspec name, but + // fallback to just using the first one. + dllEntry = dllEntries.FirstOrDefault( + dll => Path.GetFileNameWithoutExtension(nuspecEntry.Name) + .Equals(Path.GetFileNameWithoutExtension(dll.Name), StringComparison.OrdinalIgnoreCase)) ?? dllEntry; + } + + dllStream = dllEntry.Open(); + var docEntry = zipArchive.GetEntry(Path.ChangeExtension(dllEntry.FullName, ".xml")); + if (docEntry != null) + { + docStream = docEntry.Open(); + } + using var nuspecStream = nuspecEntry.Open(); + var document = XDocument.Load(nuspecStream); + var dependencyElements = document.Descendants().Where(e => e.Name.LocalName == "dependency"); + dependencies = new List(); + dependencies.AddRange( + dependencyElements.Select(dependency => new DependencyInfo( + dependency.Attribute("id")?.Value, + dependency.Attribute("version")?.Value))); + // filter duplicates and sort + if (dependencies.Any()) + { + dependencies = dependencies + .GroupBy(d => d.Name) + .Select(d => d.First()) + .OrderBy(d => d.Name).ToList(); + } + } + + var assemblySymbol = CompilationFactory.GetCompilation(dllStream, docStream); + if (assemblySymbol == null) + { + Console.Error.WriteLine($"PackageFile {packageFilePath.FullName} contains no Assembly Symbol."); + return; + } + var parsedFileName = string.IsNullOrEmpty(outputFileName) ? assemblySymbol.Name : outputFileName; + var treeTokenCodeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, runAnalysis, dependencies); + var gzipJsonTokenFilePath = Path.Combine(OutputDirectory.FullName, $"{parsedFileName}.json.tgz"); + + + var options = new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + { + using FileStream gzipFileStream = new FileStream(gzipJsonTokenFilePath, FileMode.Create, FileAccess.Write); + using GZipStream gZipStream = new GZipStream(gzipFileStream, CompressionLevel.Optimal); + JsonSerializer.Serialize(new Utf8JsonWriter(gZipStream, new JsonWriterOptions { Indented = false }), treeTokenCodeFile, options); + } + + Console.WriteLine($"TokenCodeFile File {gzipJsonTokenFilePath} Generated Successfully."); + Console.WriteLine(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error Parsing PackageFile : {ex.Message}"); + } + finally + { + zipArchive?.Dispose(); + } + +} + +static bool IsNuget(string name) +{ + return name.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase); +} + +static bool IsNuspec(string name) +{ + return name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase); +} + +static bool IsDll(string name) +{ + return name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase); +} diff --git a/tools/apiview/parsers/csharp-api-parser/TreeToken/CodeFileBuilder.cs b/tools/apiview/parsers/csharp-api-parser/TreeToken/CodeFileBuilder.cs new file mode 100644 index 00000000000..0660e7aad4c --- /dev/null +++ b/tools/apiview/parsers/csharp-api-parser/TreeToken/CodeFileBuilder.cs @@ -0,0 +1,813 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using ApiView; +using APIView.Analysis; +using APIView.TreeToken; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.SymbolDisplay; +using System.Collections.Immutable; +using System.ComponentModel; + +namespace CSharpAPIParser.TreeToken +{ + internal class CodeFileBuilder + { + private static readonly char[] _newlineChars = new char[] { '\r', '\n' }; + + SymbolDisplayFormat _defaultDisplayFormat = new SymbolDisplayFormat( + SymbolDisplayGlobalNamespaceStyle.Omitted, + delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature, + extensionMethodStyle: SymbolDisplayExtensionMethodStyle.StaticMethod, + propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral | + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.RemoveAttributeSuffix | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier, + kindOptions: SymbolDisplayKindOptions.IncludeMemberKeyword, + parameterOptions: SymbolDisplayParameterOptions.IncludeDefaultValue | + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeName | + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeType, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeVariance, + memberOptions: SymbolDisplayMemberOptions.IncludeExplicitInterface | + SymbolDisplayMemberOptions.IncludeConstantValue | + SymbolDisplayMemberOptions.IncludeModifiers | + SymbolDisplayMemberOptions.IncludeParameters | + SymbolDisplayMemberOptions.IncludeType + ); + + private IAssemblySymbol? _assembly; + + public ICodeFileBuilderSymbolOrderProvider SymbolOrderProvider { get; set; } = new CodeFileBuilderSymbolOrderProvider(); + + public const string CurrentVersion = "27"; + + private IEnumerable EnumerateNamespaces(IAssemblySymbol assemblySymbol) + { + var stack = new Stack(); + stack.Push(assemblySymbol.GlobalNamespace); + while (stack.TryPop(out var currentNamespace)) + { + if (HasAnyPublicTypes(currentNamespace)) + { + yield return currentNamespace; + } + foreach (var subNamespace in currentNamespace.GetNamespaceMembers()) + { + stack.Push(subNamespace); + } + } + } + + public CodeFile Build(IAssemblySymbol assemblySymbol, bool runAnalysis, List? dependencies) + { + _assembly = assemblySymbol; + var analyzer = new Analyzer(); + + if (runAnalysis) + { + analyzer.VisitAssembly(assemblySymbol); + } + + var apiTreeNode = new APITreeNode(); + apiTreeNode.Kind = APITreeNode.ASSEMBLY; + apiTreeNode.Id = assemblySymbol.Name; + apiTreeNode.Name = assemblySymbol.Name + ".dll"; + + if (dependencies != null) + { + BuildDependencies(apiTreeNode.ChildrenObj, dependencies); + } + BuildInternalsVisibleToAttributes(apiTreeNode.ChildrenObj, assemblySymbol); + + foreach (var namespaceSymbol in SymbolOrderProvider.OrderNamespaces(EnumerateNamespaces(assemblySymbol))) + { + if (namespaceSymbol.IsGlobalNamespace) + { + foreach (var namedTypeSymbol in SymbolOrderProvider.OrderTypes(namespaceSymbol.GetTypeMembers())) + { + BuildType(apiTreeNode.ChildrenObj, namedTypeSymbol, false); + } + } + else + { + BuildNamespace(apiTreeNode.ChildrenObj, namespaceSymbol); + } + } + + // Sort API Tree by name + apiTreeNode.SortChildren(); + + var treeTokenCodeFile = new CodeFile() + { + Name = $"{assemblySymbol.Name} ({assemblySymbol.Identity.Version})", + Language = "C#", + APIForest = new List() { apiTreeNode }, + VersionString = CurrentVersion, + Diagnostics = analyzer.Results.ToArray(), + PackageName = assemblySymbol.Name, + PackageVersion = assemblySymbol.Identity.Version.ToString() + }; + + return treeTokenCodeFile; + } + + public static void BuildInternalsVisibleToAttributes(List apiTree, IAssemblySymbol assemblySymbol) + { + var assemblyAttributes = assemblySymbol.GetAttributes() + .Where(a => + a.AttributeClass?.Name == "InternalsVisibleToAttribute" && + !a.ConstructorArguments[0].Value?.ToString()?.Contains(".Tests") == true && + !a.ConstructorArguments[0].Value?.ToString()?.Contains(".Perf") == true && + !a.ConstructorArguments[0].Value?.ToString()?.Contains("DynamicProxyGenAssembly2") == true); + if (assemblyAttributes != null && assemblyAttributes.Any()) + { + var apiTreeNode = new APITreeNode(); + apiTreeNode.Kind = apiTreeNode.Name = apiTreeNode.Id = APITreeNode.INTERNALS_VISIBLE_TO; + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateTextToken(value: "Exposes internals to:")); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateLineBreakToken()); + + foreach (AttributeData attribute in assemblyAttributes) + { + if (attribute.ConstructorArguments.Length > 0) + { + var param = attribute.ConstructorArguments[0].Value?.ToString(); + if (!String.IsNullOrEmpty(param)) + { + var firstComma = param?.IndexOf(','); + param = firstComma > 0 ? param?[..(int)firstComma] : param; + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateTextToken(value: param, id: attribute.AttributeClass?.Name)); + } + } + } + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreateEmptyToken()); + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreateLineBreakToken()); + apiTree.Add(apiTreeNode); + } + } + + public static void BuildDependencies(List apiTree, List dependencies) + { + if (dependencies != null && dependencies.Any()) + { + var apiTreeNode = new APITreeNode(); + apiTreeNode.Kind = apiTreeNode.Name = apiTreeNode.Id = APITreeNode.DEPENDENCIES; + + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateLineBreakToken()); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateTextToken(value: "Dependencies:")); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateLineBreakToken()); + + foreach (DependencyInfo dependency in dependencies) + { + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateTextToken(value: dependency.Name, id: dependency.Name)); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateTextToken(value: $"-{dependency.Version}")); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateLineBreakToken()); + } + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreateEmptyToken()); + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreateLineBreakToken()); + apiTree.Add(apiTreeNode); + } + } + + private void BuildNamespace(List apiTree, INamespaceSymbol namespaceSymbol) + { + bool isHidden = HasOnlyHiddenTypes(namespaceSymbol); + + var apiTreeNode = new APITreeNode(); + apiTreeNode.Id = namespaceSymbol.GetId(); + apiTreeNode.Name = namespaceSymbol.ToDisplayString(); + apiTreeNode.Kind = APITreeNode.NAMESPACE; + + if (isHidden) + { + apiTreeNode.TagsObj.Add(APITreeNode.HIDDEN); + } + + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateKeywordToken(SyntaxKind.NamespaceKeyword)); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateSpaceToken()); + BuildNamespaceName(apiTreeNode.TopTokensObj, namespaceSymbol); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateSpaceToken()); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.OpenBraceToken)); + + foreach (var namedTypeSymbol in SymbolOrderProvider.OrderTypes(namespaceSymbol.GetTypeMembers())) + { + BuildType(apiTreeNode.ChildrenObj, namedTypeSymbol, isHidden); + } + + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CloseBraceToken)); + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreateLineBreakToken()); + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreateEmptyToken()); + + apiTree.Add(apiTreeNode); + } + + private void BuildNamespaceName(List tokenList, INamespaceSymbol namespaceSymbol) + { + if (!namespaceSymbol.ContainingNamespace.IsGlobalNamespace) + { + BuildNamespaceName(tokenList, namespaceSymbol.ContainingNamespace); + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.DotToken)); + } + DisplayName(tokenList, namespaceSymbol, namespaceSymbol); + } + + + private bool HasAnyPublicTypes(INamespaceSymbol subNamespaceSymbol) + { + return subNamespaceSymbol.GetTypeMembers().Any(IsAccessible); + } + + private bool HasOnlyHiddenTypes(INamespaceSymbol namespaceSymbol) + { + return namespaceSymbol.GetTypeMembers().All(t => IsHiddenFromIntellisense(t) || !IsAccessible(t)); + } + + private void BuildType(List apiTree, INamedTypeSymbol namedType, bool inHiddenScope) + { + if (!IsAccessible(namedType)) + { + return; + } + + bool isHidden = IsHiddenFromIntellisense(namedType); + var apiTreeNode = new APITreeNode(); + apiTreeNode.Kind = APITreeNode.TYPE; + apiTreeNode.PropertiesObj.Add(APITreeNode.SUB_KIND, namedType.TypeKind.ToString().ToLowerInvariant()); + apiTreeNode.Id = namedType.GetId(); + apiTreeNode.Name = namedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + + if (isHidden && !inHiddenScope) + { + apiTreeNode.TagsObj.Add(APITreeNode.HIDDEN); + } + + BuildDocumentation(apiTreeNode.TopTokensObj, namedType); + BuildAttributes(apiTreeNode.TopTokensObj, namedType.GetAttributes()); + BuildVisibility(apiTreeNode.TopTokensObj, namedType); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateSpaceToken()); + + switch (namedType.TypeKind) + { + case TypeKind.Class: + BuildClassModifiers(apiTreeNode.TopTokensObj, namedType); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateKeywordToken(SyntaxKind.ClassKeyword)); + break; + case TypeKind.Delegate: + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateKeywordToken(SyntaxKind.DelegateKeyword)); + break; + case TypeKind.Enum: + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateKeywordToken(SyntaxKind.EnumKeyword)); + break; + case TypeKind.Interface: + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateKeywordToken(SyntaxKind.InterfaceKeyword)); + break; + case TypeKind.Struct: + if (namedType.IsReadOnly) + { + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateKeywordToken(SyntaxKind.ReadOnlyKeyword)); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateSpaceToken()); + } + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateKeywordToken(SyntaxKind.StructKeyword)); + break; + } + + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateSpaceToken()); + DisplayName(apiTreeNode.TopTokensObj, namedType, namedType); + + if (namedType.TypeKind == TypeKind.Delegate) + { + apiTreeNode.TopTokensObj.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.SemicolonToken)); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateLineBreakToken()); + return; + } + + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateSpaceToken()); + BuildBaseType(apiTreeNode.TopTokensObj, namedType); + apiTreeNode.TopTokensObj.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.OpenBraceToken)); + + foreach (var namedTypeSymbol in SymbolOrderProvider.OrderTypes(namedType.GetTypeMembers())) + { + BuildType(apiTreeNode.ChildrenObj, namedTypeSymbol, inHiddenScope || isHidden); + } + + foreach (var member in SymbolOrderProvider.OrderMembers(namedType.GetMembers())) + { + if (member.Kind == SymbolKind.NamedType || member.IsImplicitlyDeclared || !IsAccessible(member)) continue; + if (member is IMethodSymbol method) + { + if (method.MethodKind == MethodKind.PropertyGet || + method.MethodKind == MethodKind.PropertySet || + method.MethodKind == MethodKind.EventAdd || + method.MethodKind == MethodKind.EventRemove || + method.MethodKind == MethodKind.EventRaise) + { + continue; + } + } + BuildMember(apiTreeNode.ChildrenObj, member, inHiddenScope); + } + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CloseBraceToken)); + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreateLineBreakToken()); + apiTreeNode.BottomTokensObj.Add(StructuredToken.CreateEmptyToken()); + + apiTree.Add(apiTreeNode); + } + + private void BuildDocumentation(List tokenList, ISymbol symbol) + { + var lines = symbol.GetDocumentationCommentXml()?.Trim().Split(_newlineChars); + + if (lines != null) + { + if (lines.All(string.IsNullOrWhiteSpace)) + { + return; + } + foreach (var line in lines) + { + var docToken = new StructuredToken("// " + line.Trim()); + docToken.RenderClassesObj.Add("comment"); + docToken.PropertiesObj.Add(StructuredToken.GROUP_ID, StructuredToken.DOCUMENTATION); + tokenList.Add(docToken); + tokenList.Add(StructuredToken.CreateLineBreakToken()); + } + } + } + + private static void BuildClassModifiers(List tokenList, INamedTypeSymbol namedType) + { + if (namedType.IsAbstract) + { + tokenList.Add(StructuredToken.CreateKeywordToken(SyntaxKind.AbstractKeyword)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + } + + if (namedType.IsStatic) + { + tokenList.Add(StructuredToken.CreateKeywordToken(SyntaxKind.StaticKeyword)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + } + + if (namedType.IsSealed) + { + tokenList.Add(StructuredToken.CreateKeywordToken(SyntaxKind.SealedKeyword)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + } + } + + private void BuildBaseType(List tokenList, INamedTypeSymbol namedType) + { + bool first = true; + + if (namedType.BaseType != null && + namedType.BaseType.SpecialType == SpecialType.None) + { + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.ColonToken)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + first = false; + + DisplayName(tokenList, namedType.BaseType); + } + + foreach (var typeInterface in namedType.Interfaces) + { + if (!IsAccessible(typeInterface)) continue; + + if (!first) + { + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CommaToken)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + } + else + { + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.ColonToken)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + first = false; + } + + DisplayName(tokenList, typeInterface); + } + + if (!first) + { + tokenList.Add(StructuredToken.CreateSpaceToken()); + } + } + + private void BuildMember(List apiTree, ISymbol member, bool inHiddenScope) + { + bool isHidden = IsHiddenFromIntellisense(member); + var apiTreeNode = new APITreeNode(); + apiTreeNode.Kind = APITreeNode.MEMBER; + apiTreeNode.PropertiesObj.Add(APITreeNode.SUB_KIND, member.Kind.ToString()); + apiTreeNode.Id = member.GetId(); + apiTreeNode.Name = member.ToDisplayString(); + apiTreeNode.TagsObj.Add(APITreeNode.HIDE_FROM_NAV); + + if (isHidden && !inHiddenScope) + { + apiTreeNode.TagsObj.Add(APITreeNode.HIDDEN); + } + + BuildDocumentation(apiTreeNode.TopTokensObj, member); + BuildAttributes(apiTreeNode.TopTokensObj, member.GetAttributes()); + DisplayName(apiTreeNode.TopTokensObj, member); + + if (member.Kind == SymbolKind.Field && member.ContainingType.TypeKind == TypeKind.Enum) + { + apiTreeNode.TopTokensObj.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CommaToken)); + } + else if (member.Kind != SymbolKind.Property) + { + apiTreeNode.TopTokensObj.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.SemicolonToken)); + } + + apiTreeNode.TopTokensObj.Add(StructuredToken.CreateLineBreakToken()); + apiTree.Add(apiTreeNode); + } + + private void BuildAttributes(List tokenList, ImmutableArray attributes) + { + const string attributeSuffix = "Attribute"; + foreach (var attribute in attributes) + { + if (attribute.AttributeClass != null) + { + if ((!IsAccessible(attribute.AttributeClass) && + attribute.AttributeClass.Name != "FriendAttribute" && + attribute.AttributeClass.ContainingNamespace.ToString() != "System.Diagnostics.CodeAnalysis") + || IsSkippedAttribute(attribute.AttributeClass)) + { + continue; + } + + if (attribute.AttributeClass.DeclaredAccessibility == Accessibility.Internal || attribute.AttributeClass.DeclaredAccessibility == Accessibility.Friend) + { + tokenList.Add(StructuredToken.CreateKeywordToken("internal")); + tokenList.Add(StructuredToken.CreateSpaceToken()); + } + + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.OpenBracketToken)); + var name = attribute.AttributeClass.Name; + if (name.EndsWith(attributeSuffix)) + { + name = name.Substring(0, name.Length - attributeSuffix.Length); + } + tokenList.Add(StructuredToken.CreateTypeNameToken(name)); + if (attribute.ConstructorArguments.Any()) + { + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.OpenParenToken)); + bool first = true; + + foreach (var argument in attribute.ConstructorArguments) + { + if (!first) + { + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CommaToken)); + tokenList.Add(StructuredToken.CreateParameterSeparatorToken()); + } + else + { + first = false; + } + BuildTypedConstant(tokenList, argument); + } + + foreach (var argument in attribute.NamedArguments) + { + if (!first) + { + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CommaToken)); + tokenList.Add(StructuredToken.CreateParameterSeparatorToken()); + } + else + { + first = false; + } + tokenList.Add(StructuredToken.CreateTextToken(argument.Key)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.EqualsToken)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + BuildTypedConstant(tokenList, argument.Value); + } + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CloseParenToken)); + } + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CloseBracketToken)); + tokenList.Add(StructuredToken.CreateLineBreakToken()); + } + } + } + + private bool IsSkippedAttribute(INamedTypeSymbol attributeAttributeClass) + { + switch (attributeAttributeClass.Name) + { + case "DebuggerStepThroughAttribute": + case "AsyncStateMachineAttribute": + case "IteratorStateMachineAttribute": + case "DefaultMemberAttribute": + case "AsyncIteratorStateMachineAttribute": + case "EditorBrowsableAttribute": + case "NullableAttribute": + case "NullableContextAttribute": + return true; + default: + return false; + } + } + + private bool IsHiddenFromIntellisense(ISymbol member) => + member.GetAttributes().Any(d => d.AttributeClass?.Name == "EditorBrowsableAttribute" + && (EditorBrowsableState)d.ConstructorArguments[0].Value! == EditorBrowsableState.Never); + + private bool IsDecoratedWithAttribute(ISymbol member, string attributeName) => + member.GetAttributes().Any(d => d.AttributeClass?.Name == attributeName); + + private void BuildTypedConstant(List tokenList, TypedConstant typedConstant) + { + if (typedConstant.IsNull) + { + tokenList.Add(StructuredToken.CreateKeywordToken(SyntaxKind.NullKeyword)); + } + else if (typedConstant.Kind == TypedConstantKind.Enum) + { + new CodeFileBuilderEnumFormatter(tokenList).Format(typedConstant.Type, typedConstant.Value); + } + else if (typedConstant.Kind == TypedConstantKind.Type) + { + tokenList.Add(StructuredToken.CreateKeywordToken(SyntaxKind.TypeOfKeyword)); + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.OpenParenToken)); + DisplayName(tokenList, (ITypeSymbol)typedConstant.Value!); + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CloseParenToken)); + } + else if (typedConstant.Kind == TypedConstantKind.Array) + { + tokenList.Add(StructuredToken.CreateKeywordToken(SyntaxKind.NewKeyword)); + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.OpenBracketToken)); + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CloseBracketToken)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.OpenBraceToken)); + + bool first = true; + + foreach (var value in typedConstant.Values) + { + if (!first) + { + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CommaToken)); + tokenList.Add(StructuredToken.CreateSpaceToken()); + } + else + { + first = false; + } + + BuildTypedConstant(tokenList, value); + } + tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.CloseBraceToken)); + } + else + { + if (typedConstant.Value is string s) + { + tokenList.Add(StructuredToken.CreateStringLiteralToken(ObjectDisplay.FormatLiteral(s, ObjectDisplayOptions.UseQuotes | ObjectDisplayOptions.EscapeNonPrintableCharacters))); + } + else + { + tokenList.Add(StructuredToken.CreateLiteralToken(ObjectDisplay.FormatPrimitive(typedConstant.Value, ObjectDisplayOptions.None))); + } + } + } + + private void BuildVisibility(List tokenList, ISymbol symbol) + { + tokenList.Add(StructuredToken.CreateKeywordToken(ToEffectiveAccessibility(symbol.DeclaredAccessibility))); + } + + private void DisplayName(List tokenList, ISymbol symbol, ISymbol? definedSymbol = null) + { + tokenList.Add(StructuredToken.CreateEmptyToken(id: symbol.GetId())); + if (NeedsAccessibility(symbol)) + { + tokenList.Add(StructuredToken.CreateKeywordToken(ToEffectiveAccessibility(symbol.DeclaredAccessibility))); + tokenList.Add(StructuredToken.CreateSpaceToken()); + } + if (symbol is IPropertySymbol propSymbol && propSymbol.DeclaredAccessibility != Accessibility.Internal) + { + SymbolDisplayPart previous = default(SymbolDisplayPart); + var parts = propSymbol.ToDisplayParts(_defaultDisplayFormat); + for (int i = 0; i < parts.Length; i++) + { + // Skip internal setters + if (parts[i].Kind == SymbolDisplayPartKind.Keyword && parts[i].ToString() == "internal") + { + while (i < parts.Length && parts[i].ToString() != "}") + { + i++; + } + } + tokenList.Add(MapToken(definedSymbol: definedSymbol!, symbolDisplayPart: parts[i], + previousSymbolDisplayPart: previous)); + previous = parts[i]; + } + } + else + { + SymbolDisplayPart previous = default(SymbolDisplayPart); + foreach (var symbolDisplayPart in symbol.ToDisplayParts(_defaultDisplayFormat)) + { + tokenList.Add(MapToken(definedSymbol: definedSymbol!, symbolDisplayPart: symbolDisplayPart, + previousSymbolDisplayPart: previous)); + previous = symbolDisplayPart; + } + } + } + + private bool NeedsAccessibility(ISymbol symbol) + { + return symbol switch + { + INamespaceSymbol => false, + INamedTypeSymbol => false, + IFieldSymbol fieldSymbol => fieldSymbol.ContainingType.TypeKind != TypeKind.Enum, + IMethodSymbol methodSymbol => !methodSymbol.ExplicitInterfaceImplementations.Any() && + methodSymbol.ContainingType.TypeKind != TypeKind.Interface, + IPropertySymbol propertySymbol => !propertySymbol.ExplicitInterfaceImplementations.Any() && + propertySymbol.ContainingType.TypeKind != TypeKind.Interface, + _ => true + }; + } + + private StructuredToken MapToken(ISymbol definedSymbol, SymbolDisplayPart symbolDisplayPart, SymbolDisplayPart previousSymbolDisplayPart) + { + string? navigateToId = null; + var symbol = symbolDisplayPart.Symbol; + + if (symbol is INamedTypeSymbol && + (definedSymbol == null || !SymbolEqualityComparer.Default.Equals(definedSymbol, symbol)) && + SymbolEqualityComparer.Default.Equals(_assembly, symbol.ContainingAssembly)) + { + navigateToId = symbol.GetId(); + } + + var definitionId = (definedSymbol != null && SymbolEqualityComparer.Default.Equals(definedSymbol, symbol)) ? definedSymbol.GetId() : null; + var tokenValue = symbolDisplayPart.ToString(); + + StructuredToken? token = null; + + switch (symbolDisplayPart.Kind) + { + case SymbolDisplayPartKind.TypeParameterName: + case SymbolDisplayPartKind.AliasName: + case SymbolDisplayPartKind.AssemblyName: + case SymbolDisplayPartKind.ClassName: + case SymbolDisplayPartKind.DelegateName: + case SymbolDisplayPartKind.EnumName: + case SymbolDisplayPartKind.ErrorTypeName: + case SymbolDisplayPartKind.InterfaceName: + case SymbolDisplayPartKind.StructName: + token = StructuredToken.CreateTypeNameToken(tokenValue); + break; + case SymbolDisplayPartKind.Keyword: + token = StructuredToken.CreateKeywordToken(tokenValue); + break; + case SymbolDisplayPartKind.LineBreak: + token = StructuredToken.CreateLineBreakToken(); + break; + case SymbolDisplayPartKind.StringLiteral: + token = StructuredToken.CreateStringLiteralToken(tokenValue); + break; + case SymbolDisplayPartKind.Punctuation: + token = StructuredToken.CreatePunctuationToken(tokenValue); + break; + case SymbolDisplayPartKind.Space: + if (previousSymbolDisplayPart.Kind == SymbolDisplayPartKind.Punctuation && previousSymbolDisplayPart.ToString().Equals(",")) + { + token = StructuredToken.CreateParameterSeparatorToken(); + } + else + { + token = StructuredToken.CreateSpaceToken(); + } + break; + case SymbolDisplayPartKind.PropertyName: + case SymbolDisplayPartKind.EventName: + case SymbolDisplayPartKind.FieldName: + case SymbolDisplayPartKind.MethodName: + case SymbolDisplayPartKind.Operator: + case SymbolDisplayPartKind.EnumMemberName: + case SymbolDisplayPartKind.ExtensionMethodName: + case SymbolDisplayPartKind.ConstantName: + token = StructuredToken.CreateMemberNameToken(tokenValue); + break; + default: + token = StructuredToken.CreateTextToken(tokenValue); + break; + } + + if (!String.IsNullOrWhiteSpace(definitionId)) + { + token.Id = definitionId!; + } + + if (!String.IsNullOrWhiteSpace(navigateToId)) + { + token.PropertiesObj.Add(StructuredToken.NAVIGATE_TO_ID, navigateToId!); + } + + return token; + } + + private Accessibility ToEffectiveAccessibility(Accessibility accessibility) + { + switch (accessibility) + { + case Accessibility.ProtectedAndInternal: + return Accessibility.Internal; + case Accessibility.ProtectedOrInternal: + return Accessibility.Protected; + default: + return accessibility; + } + } + + private bool IsAccessible(ISymbol s) + { + switch (s.DeclaredAccessibility) + { + case Accessibility.Protected: + case Accessibility.ProtectedOrInternal: + case Accessibility.Public: + return true; + case Accessibility.Internal: + return s.GetAttributes().Any(a => a.AttributeClass.Name == "FriendAttribute"); + default: + return IsAccessibleExplicitInterfaceImplementation(s); + } + } + + private bool IsAccessibleExplicitInterfaceImplementation(ISymbol s) + { + return s switch + { + IMethodSymbol methodSymbol => methodSymbol.ExplicitInterfaceImplementations.Any(i => IsAccessible(i.ContainingType)), + IPropertySymbol propertySymbol => propertySymbol.ExplicitInterfaceImplementations.Any(i => IsAccessible(i.ContainingType)), + _ => false + }; + } + + internal class CodeFileBuilderEnumFormatter : AbstractSymbolDisplayVisitor + { + private readonly List _tokenList; + + public CodeFileBuilderEnumFormatter(List tokenList) : base(null, SymbolDisplayFormat.FullyQualifiedFormat, false, null, 0, false) + { + _tokenList = tokenList; + } + + protected override AbstractSymbolDisplayVisitor MakeNotFirstVisitor(bool inNamespaceOrType = false) + { + return this; + } + + protected override void AddLiteralValue(SpecialType type, object value) + { + _tokenList.Add(StructuredToken.CreateLiteralToken(ObjectDisplay.FormatPrimitive(value, ObjectDisplayOptions.None))); + } + + protected override void AddExplicitlyCastedLiteralValue(INamedTypeSymbol namedType, SpecialType type, object value) + { + _tokenList.Add(StructuredToken.CreateLiteralToken(ObjectDisplay.FormatPrimitive(value, ObjectDisplayOptions.None))); + } + + protected override void AddSpace() + { + _tokenList.Add(StructuredToken.CreateSpaceToken()); + } + + protected override void AddBitwiseOr() + { + _tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.BarToken)); + } + + public override void VisitField(IFieldSymbol symbol) + { + _tokenList.Add(StructuredToken.CreateTypeNameToken(symbol.Type.Name)); + _tokenList.Add(StructuredToken.CreatePunctuationToken(SyntaxKind.DotToken)); + _tokenList.Add(StructuredToken.CreateMemberNameToken(symbol.Name)); + } + + public void Format(ITypeSymbol? type, object? typedConstantValue) + { + AddNonNullConstantValue(type, typedConstantValue); + } + } + } +} diff --git a/tools/apiview/parsers/csharp-api-parser/csharp-api-parser.sln b/tools/apiview/parsers/csharp-api-parser/csharp-api-parser.sln new file mode 100644 index 00000000000..ad79aafb026 --- /dev/null +++ b/tools/apiview/parsers/csharp-api-parser/csharp-api-parser.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34601.278 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "APIView", "..\..\..\..\src\dotnet\APIView\APIView\APIView.csproj", "{B12E5F4E-D63D-411F-A22D-9DD0DE1769A3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.ClientSdk.Analyzers", "..\..\..\..\src\dotnet\Azure.ClientSdk.Analyzers\Azure.ClientSdk.Analyzers\Azure.ClientSdk.Analyzers.csproj", "{4ECD9A58-ABEB-4285-A5D7-7CDA8A558AE0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpAPIParser", "CSharpAPIParser.csproj", "{EB159E3D-9732-4EB3-A3C2-817475B9D749}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B12E5F4E-D63D-411F-A22D-9DD0DE1769A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B12E5F4E-D63D-411F-A22D-9DD0DE1769A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B12E5F4E-D63D-411F-A22D-9DD0DE1769A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B12E5F4E-D63D-411F-A22D-9DD0DE1769A3}.Release|Any CPU.Build.0 = Release|Any CPU + {4ECD9A58-ABEB-4285-A5D7-7CDA8A558AE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4ECD9A58-ABEB-4285-A5D7-7CDA8A558AE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4ECD9A58-ABEB-4285-A5D7-7CDA8A558AE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4ECD9A58-ABEB-4285-A5D7-7CDA8A558AE0}.Release|Any CPU.Build.0 = Release|Any CPU + {EB159E3D-9732-4EB3-A3C2-817475B9D749}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB159E3D-9732-4EB3-A3C2-817475B9D749}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB159E3D-9732-4EB3-A3C2-817475B9D749}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB159E3D-9732-4EB3-A3C2-817475B9D749}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D2777671-F5DD-4279-BF51-B9C81E05BD5A} + EndGlobalSection +EndGlobal diff --git a/tools/apiview/parsers/images/APITree.svg b/tools/apiview/parsers/images/APITree.svg new file mode 100644 index 00000000000..eeeaf3fde9c --- /dev/null +++ b/tools/apiview/parsers/images/APITree.svg @@ -0,0 +1,211 @@ + + + + + + + + + + Page-1 + + + Rectangle.1000 + + + + + + + Rectangle + + + + + + + Rectangle.11 + + + + + + + Rectangle.12 + + + + + + + Rectangle.13 + + + + + + + Rectangle.14 + + + + + + + Rectangle.15 + + + + + + + Sheet.16 + namespace + + + + namespace + + Sheet.18 + } + + + + } + + Sheet.19 + Public + + + + Public + + Sheet.20 + } + + + + } + + Sheet.21 + Public + + + + Public + + Sheet.22 + Azure.Template + + + + Azure.Template + + Sheet.23 + + + + Sheet.25 + + + + Sheet.26 + { + + + + { + + Sheet.27 + TemplateClient + + + + TemplateClient + + Sheet.28 + + + + Sheet.29 + class + + + + class + + Sheet.30 + + + + Sheet.31 + TemplateClient + + + + TemplateClient + + Sheet.33 + + + + Sheet.34 + ( + + + + ( + + Sheet.35 + string + + + + string + + Sheet.36 + vaultBaseUrl + + + + vaultBaseUrl + + Sheet.37 + ) + + + + ) + + Sheet.38 + ; + + + + ; + + Sheet.39 + Top Token + + + + Top Token + + Sheet.40 + Bottom Token + + + + Bottom Token + + diff --git a/tools/apiview/parsers/images/apitree-node-and-tokens.png b/tools/apiview/parsers/images/apitree-node-and-tokens.png new file mode 100644 index 00000000000..3bff57c865c Binary files /dev/null and b/tools/apiview/parsers/images/apitree-node-and-tokens.png differ diff --git a/tools/apiview/parsers/images/deprecated-node.png b/tools/apiview/parsers/images/deprecated-node.png new file mode 100644 index 00000000000..24dc34235cc Binary files /dev/null and b/tools/apiview/parsers/images/deprecated-node.png differ diff --git a/tools/apiview/parsers/images/deprecated-token.png b/tools/apiview/parsers/images/deprecated-token.png new file mode 100644 index 00000000000..578b53d151b Binary files /dev/null and b/tools/apiview/parsers/images/deprecated-token.png differ diff --git a/tools/apiview/parsers/images/documentation.png b/tools/apiview/parsers/images/documentation.png new file mode 100644 index 00000000000..59ae60a36e2 Binary files /dev/null and b/tools/apiview/parsers/images/documentation.png differ diff --git a/tools/apiview/parsers/images/hidden-api.png b/tools/apiview/parsers/images/hidden-api.png new file mode 100644 index 00000000000..dd35e0b4bfa Binary files /dev/null and b/tools/apiview/parsers/images/hidden-api.png differ diff --git a/tools/apiview/parsers/images/navigation.png b/tools/apiview/parsers/images/navigation.png new file mode 100644 index 00000000000..38ed23051c6 Binary files /dev/null and b/tools/apiview/parsers/images/navigation.png differ diff --git a/tools/apiview/parsers/images/parameter-separator-with-line-break.png b/tools/apiview/parsers/images/parameter-separator-with-line-break.png new file mode 100644 index 00000000000..e6b1372278b Binary files /dev/null and b/tools/apiview/parsers/images/parameter-separator-with-line-break.png differ diff --git a/tools/apiview/parsers/images/parameter-separator-with-single-space.png b/tools/apiview/parsers/images/parameter-separator-with-single-space.png new file mode 100644 index 00000000000..71b5c25a895 Binary files /dev/null and b/tools/apiview/parsers/images/parameter-separator-with-single-space.png differ diff --git a/tools/apiview/parsers/images/url-token.png b/tools/apiview/parsers/images/url-token.png new file mode 100644 index 00000000000..c5545b95bd1 Binary files /dev/null and b/tools/apiview/parsers/images/url-token.png differ