diff --git a/plugins/csharp/parser/src_csharp/AstVisitor.cs b/plugins/csharp/parser/src_csharp/AstVisitor.cs index 5b24a511b..057dfffc3 100644 --- a/plugins/csharp/parser/src_csharp/AstVisitor.cs +++ b/plugins/csharp/parser/src_csharp/AstVisitor.cs @@ -56,14 +56,35 @@ private ulong fnvHash(string data_) } return hash; - } + } + + // Deterministic replacement for GetHashCode(), which is reference based + // for syntax nodes and randomized per process for strings. Both would + // give different ids for the same entity on every parse. + private long stableHash(string data_) + { + return unchecked((long)fnvHash(data_)); + } + + private long getEntityHash(SyntaxNode node) + { + string[] properties = + { + node.SyntaxTree.FilePath,":", + node.Kind().ToString(),":", + node.Span.Start.ToString(),":", + node.Span.End.ToString() + }; + + return stableHash(string.Concat(properties)); + } private ulong getAstNodeId(SyntaxNode node){ CsharpAstNode astNode = new CsharpAstNode { AstValue = node.ToString(), RawKind = node.Kind(), - EntityHash = node.GetHashCode(), + EntityHash = getEntityHash(node), AstType = AstTypeEnum.Declaration }; astNode.SetLocation(node.SyntaxTree.GetLineSpan(node.Span)); @@ -86,7 +107,7 @@ private CsharpAstNode AstNode(SyntaxNode node, AstSymbolTypeEnum type, AstTypeEn { AstValue = node.ToString(), RawKind = node.Kind(), - EntityHash = node.GetHashCode(), + EntityHash = getEntityHash(node), AstSymbolType = type, AstType = astType, Accessibility = acc @@ -616,7 +637,7 @@ private void VisitMethodDecl(MethodDeclarationSyntax node, CsharpAstNode parent) QualifiedName = qName, QualifiedType = qType, DocumentationCommentXML = Model.GetDeclaredSymbol(node).GetDocumentationCommentXml(), - TypeHash = qType.GetHashCode(), + TypeHash = stableHash(qType), EntityHash = astNode.EntityHash, ParentNode = parent, MethodType = MethodTypeEnum.Method @@ -666,7 +687,7 @@ private void VisitOperatorDecl(OperatorDeclarationSyntax node, CsharpAstNode par QualifiedName = qName, QualifiedType = qType, DocumentationCommentXML = Model.GetDeclaredSymbol(node).GetDocumentationCommentXml(), - TypeHash = qType.GetHashCode(), + TypeHash = stableHash(qType), EntityHash = astNode.EntityHash, ParentNode = parent, MethodType = MethodTypeEnum.Operator @@ -704,7 +725,7 @@ private void VisitMethodParameters(SeparatedSyntaxList paramete AstNode = astNode, Name = param.Identifier.Text, QualifiedType = paramQType, - TypeHash = paramQType.GetHashCode(), + TypeHash = stableHash(paramQType), EntityHash = astNode.EntityHash, ParentNode = parent, VariableType = VariableTypeEnum.Parameter @@ -752,7 +773,7 @@ private void VisitVariableDecl(VariableDeclarationSyntax node, CsharpAstNode par AstNode = astNode, Name = variable.Identifier.Text, QualifiedType = varQType, - TypeHash = varQType.GetHashCode(), + TypeHash = stableHash(varQType), DocumentationCommentXML = Model.GetDeclaredSymbol(variable).GetDocumentationCommentXml(), EntityHash = astNode.EntityHash, VariableType = isLINQvar ? VariableTypeEnum.LINQ : VariableTypeEnum.Variable, @@ -779,7 +800,7 @@ private void VisitPropertyDecl(PropertyDeclarationSyntax node, CsharpAstNode par AstNode = astNode, Name = node.Identifier.Text, QualifiedType = varQType, - TypeHash = varQType.GetHashCode(), + TypeHash = stableHash(varQType), VariableType = VariableTypeEnum.Property, DocumentationCommentXML = Model.GetDeclaredSymbol(node).GetDocumentationCommentXml(), EntityHash = astNode.EntityHash, @@ -951,11 +972,69 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) DbContext.CsharpEtcEntitys.Add(invoc); } } - } - } + } + } + + VisitMethodInvocation(node); base.VisitInvocationExpression(node); - } + } + + // Records an invocation whose DeclaratorNodeId points to the declaration + // of the called method. This is what the Callee / Caller references and + // the function call diagram rely on. + private void VisitMethodInvocation(InvocationExpressionSyntax node) + { + SimpleNameSyntax name = node.Expression switch + { + MemberAccessExpressionSyntax memberAccess => memberAccess.Name, + MemberBindingExpressionSyntax memberBinding => memberBinding.Name, + SimpleNameSyntax simpleName => simpleName, + _ => null + }; + + // Unqualified Foo() calls are already recorded by VisitIdentifierName. + if (name == null || (name == node.Expression && name is IdentifierNameSyntax)) + return; + + var method = Model.GetSymbolInfo(node).Symbol as IMethodSymbol; + if (method == null) + return; + + method = (method.ReducedFrom ?? method).OriginalDefinition; + method = method.PartialImplementationPart ?? method; + + var declaration = method.DeclaringSyntaxReferences + .Select(r => r.GetSyntax()) + .FirstOrDefault(s => s is BaseMethodDeclarationSyntax); + if (declaration == null) + return; + + var type = ""; + try + { + var info = Model.GetTypeInfo(node).ConvertedType; + if (info != null) type = info.Name; + } + catch (Exception) + { + FullyParsed = false; + } + + var declaratorNodeId = getAstNodeId(declaration); + var astNode = AstNode(name, AstSymbolTypeEnum.EtcEntity, AstTypeEnum.Usage); + CsharpEtcEntity invoc = new CsharpEtcEntity + { + AstNode = astNode, + EntityHash = astNode.EntityHash, + EtcEntityType = EtcEntityTypeEnum.Invocation, + DeclaratorNodeId = declaratorNodeId, + Name = name.Identifier.Text, + QualifiedName = method.ToString(), + QualifiedType = type + }; + DbContext.CsharpEtcEntitys.Add(invoc); + } public override void VisitIdentifierName(IdentifierNameSyntax node) { diff --git a/plugins/csharp/service/CMakeLists.txt b/plugins/csharp/service/CMakeLists.txt index cfc163255..3ab817894 100644 --- a/plugins/csharp/service/CMakeLists.txt +++ b/plugins/csharp/service/CMakeLists.txt @@ -39,6 +39,7 @@ add_subdirectory(src_csharp) add_library(csharpservice SHARED src/csharpservice.cpp + src/csharpdiagram.cpp src/plugin.cpp) target_compile_options(csharpservice PUBLIC -Wno-unknown-pragmas) diff --git a/plugins/csharp/service/csharpservice.thrift b/plugins/csharp/service/csharpservice.thrift index 4d223da5e..4aea6242d 100644 --- a/plugins/csharp/service/csharpservice.thrift +++ b/plugins/csharp/service/csharpservice.thrift @@ -49,9 +49,6 @@ service CsharpService map getDiagramTypes(1:common.AstNodeId astNodeId) throws (1:common.InvalidId ex) - string getDiagram(1:common.AstNodeId astNodeId, 2:i32 diagramId) - throws (1:common.InvalidId exId, 2:common.Timeout exLong) - list getSyntaxHighlight( 1:common.FileRange range, 2:list content) diff --git a/plugins/csharp/service/include/service/csharpservice.h b/plugins/csharp/service/include/service/csharpservice.h index 3660098c9..dace0cb52 100644 --- a/plugins/csharp/service/include/service/csharpservice.h +++ b/plugins/csharp/service/include/service/csharpservice.h @@ -184,14 +184,6 @@ class CSharpQueryHandler : public CsharpServiceIf _service -> getDiagramTypes(return_, astNodeId_); } - void getDiagram( - std::string& return_, - const core::AstNodeId& astNodeId_, - const std::int32_t diagramId_) override - { - _service -> getDiagram(return_, astNodeId_, diagramId_); - } - void getSyntaxHighlight( std::vector& return_, const core::FileRange& range_, @@ -219,7 +211,7 @@ using TransportException = apache::thrift::transport::TTransportException; class CsharpServiceHandler : virtual public LanguageServiceIf { - friend class Diagram; + friend class CsharpDiagram; public: CsharpServiceHandler( @@ -323,6 +315,18 @@ class CsharpServiceHandler : virtual public LanguageServiceIf const core::FileRange& range_) override; private: + // Must be kept in sync with CSharpServiceEnums.cs. + enum DiagramType + { + FUNCTION_CALL + }; + + enum ReferenceType + { + CALLEE = 5, + CALLER = 6 + }; + std::shared_ptr _db; util::OdbTransaction _transaction; diff --git a/plugins/csharp/service/src/csharpdiagram.cpp b/plugins/csharp/service/src/csharpdiagram.cpp new file mode 100644 index 000000000..7326674cd --- /dev/null +++ b/plugins/csharp/service/src/csharpdiagram.cpp @@ -0,0 +1,214 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "csharpdiagram.h" + +namespace +{ + +/** + * Removes the annotation lines and the accessibility modifiers from the + * signature of a node, and cuts the signature before the body. + */ +std::string toSignature(std::string str_) +{ + static const std::vector accessibilityTags = { + "protected internal", "private protected", + "public", "private", "protected", "internal"}; + + std::stringstream ss(str_), result; + std::string line; + + while (std::getline(ss, line)) + if (line.find('[') != 0) + result << line << '\n'; + + str_ = result.str(); + boost::trim(str_); + + for (const auto& tag : accessibilityTags) + { + std::size_t pos = str_.find(tag); + if (pos != std::string::npos) + str_.erase(pos, tag.length() + 1); + } + + return str_.substr(0, str_.find('{')); +} + +} + +namespace cc +{ +namespace service +{ +namespace language +{ + +CsharpDiagram::CsharpDiagram( + std::shared_ptr db_, + std::shared_ptr datadir_, + const cc::webserver::ServerContext& context_) + : _db(db_), + _transaction(db_), + _projectHandler(db_, datadir_, context_) +{ +} + +void CsharpDiagram::getFunctionCallDiagram( + util::Graph& graph_, + const AstNodeInfo& centerNodeInfo_, + const std::vector& calleeNodeInfos_, + const std::vector& callerNodeInfos_) +{ + graph_.setAttribute("rankdir", "LR"); + + AstNodeInfo centerInfo = centerNodeInfo_; + centerInfo.astNodeValue = toSignature(centerInfo.astNodeValue); + util::Graph::Node centerNode = addNode(graph_, centerInfo); + decorateNode(graph_, centerNode, centerNodeDecoration); + + for (const auto& calleeNodeInfo : calleeNodeInfos_) + { + AstNodeInfo calleeInfo = calleeNodeInfo; + calleeInfo.astNodeValue = toSignature(calleeInfo.astNodeValue); + util::Graph::Node calleeNode = addNode(graph_, calleeInfo); + decorateNode(graph_, calleeNode, calleeNodeDecoration); + + if (!graph_.hasEdge(centerNode, calleeNode)) + { + util::Graph::Edge edge = graph_.createEdge(centerNode, calleeNode); + decorateEdge(graph_, edge, calleeEdgeDecoration); + } + } + + for (const auto& callerNodeInfo : callerNodeInfos_) + { + AstNodeInfo callerInfo = callerNodeInfo; + callerInfo.astNodeValue = toSignature(callerInfo.astNodeValue); + util::Graph::Node callerNode = addNode(graph_, callerInfo); + decorateNode(graph_, callerNode, callerNodeDecoration); + + if (!graph_.hasEdge(callerNode, centerNode)) + { + util::Graph::Edge edge = graph_.createEdge(callerNode, centerNode); + decorateEdge(graph_, edge, callerEdgeDecoration); + } + } + + _subgraphs.clear(); +} + +std::string CsharpDiagram::getFunctionCallLegend() +{ + util::LegendBuilder builder("Function call diagram"); + + builder.addNode("center function", centerNodeDecoration); + builder.addNode("called function", calleeNodeDecoration); + builder.addNode("caller function", callerNodeDecoration); + builder.addEdge("called", calleeEdgeDecoration); + builder.addEdge("caller", callerEdgeDecoration); + + return builder.getOutput(); +} + +util::Graph::Node CsharpDiagram::addNode( + util::Graph& graph_, + const AstNodeInfo& nodeInfo_) +{ + util::Graph::Node node + = graph_.getOrCreateNode(nodeInfo_.id, + addSubgraph(graph_, nodeInfo_.range.file)); + + graph_.setNodeAttribute(node, "label", nodeInfo_.astNodeValue); + + return node; +} + +util::Graph::Subgraph CsharpDiagram::addSubgraph( + util::Graph& graph_, + const core::FileId& fileId_) +{ + auto it = _subgraphs.find(fileId_); + + if (it != _subgraphs.end()) + return it->second; + + core::FileInfo fileInfo; + _projectHandler.getFileInfo(fileInfo, fileId_); + + util::Graph::Subgraph subgraph + = graph_.getOrCreateSubgraph("cluster_" + fileInfo.path); + + graph_.setSubgraphAttribute(subgraph, "id", fileInfo.id); + graph_.setSubgraphAttribute(subgraph, "label", fileInfo.path); + + _subgraphs.insert(it, std::make_pair(fileId_, subgraph)); + + return subgraph; +} + +void CsharpDiagram::decorateNode( + util::Graph& graph_, + const util::Graph::Node& node_, + const Decoration& decoration_) const +{ + for (const auto& attr : decoration_) + graph_.setNodeAttribute(node_, attr.first, attr.second); +} + +void CsharpDiagram::decorateEdge( + util::Graph& graph_, + const util::Graph::Edge& edge_, + const Decoration& decoration_) const +{ + for (const auto& attr : decoration_) + graph_.setEdgeAttribute(edge_, attr.first, attr.second); +} + +void CsharpDiagram::decorateSubgraph( + util::Graph& graph_, + const util::Graph::Subgraph& subgraph_, + const Decoration& decoration_) const +{ + for (const auto& attr : decoration_) + graph_.setSubgraphAttribute(subgraph_, attr.first, attr.second); +} + +const CsharpDiagram::Decoration CsharpDiagram::centerNodeDecoration = { + {"style", "filled"}, + {"fillcolor", "gold"} +}; + +const CsharpDiagram::Decoration CsharpDiagram::calleeNodeDecoration = { + {"style", "filled"}, + {"fillcolor", "lightblue"} +}; + +const CsharpDiagram::Decoration CsharpDiagram::callerNodeDecoration = { + {"style", "filled"}, + {"fillcolor", "coral"} +}; + +const CsharpDiagram::Decoration CsharpDiagram::calleeEdgeDecoration = { + {"color", "blue"} +}; + +const CsharpDiagram::Decoration CsharpDiagram::callerEdgeDecoration = { + {"color", "red"} +}; + +} +} +} diff --git a/plugins/csharp/service/src/csharpdiagram.h b/plugins/csharp/service/src/csharpdiagram.h new file mode 100644 index 000000000..62924c1b6 --- /dev/null +++ b/plugins/csharp/service/src/csharpdiagram.h @@ -0,0 +1,111 @@ +#ifndef CC_SERVICE_LANGUAGE_CSHARPDIAGRAM_H +#define CC_SERVICE_LANGUAGE_CSHARPDIAGRAM_H + +#include +#include +#include + +namespace cc +{ +namespace service +{ +namespace language +{ + +class CsharpDiagram +{ +public: + CsharpDiagram( + std::shared_ptr db_, + std::shared_ptr datadir_, + const cc::webserver::ServerContext& context_); + + /** + * In the function call diagram the nodes are methods and the edges are the + * calls between them. + */ + void getFunctionCallDiagram( + util::Graph& graph_, + const AstNodeInfo& centerNodeInfo_, + const std::vector& calleeNodeInfos_, + const std::vector& callerNodeInfos_); + + /** + * This function creates legend for the Function call diagram. + * @return The generated legend as a string in SVG format. + */ + std::string getFunctionCallLegend(); + +private: + typedef std::vector> Decoration; + + /** + * This function adds a node which represents an AST node. The label of the + * node is the AST node value. A node associated with the AST node is added + * only once. If already added then the previous one is returned. + */ + util::Graph::Node addNode( + util::Graph& graph_, + const AstNodeInfo& nodeInfo_); + + /** + * This function adds a subgraph which represents a file. The label of the + * subgraph will be the file path and the subgraph will have a border. A + * subgraph associated with the file is added only once. If already added then + * the previous one is returned. + */ + util::Graph::Subgraph addSubgraph( + util::Graph& graph_, + const core::FileId& fileId_); + + /** + * This function decorates a graph node. + * @param graph_ A graph object. + * @param node_ A graph node + * @param decoration_ A map which describes the style attributes. + */ + void decorateNode( + util::Graph& graph_, + const util::Graph::Node& node_, + const Decoration& decoration_) const; + + /** + * This function decorates a graph edge. + * @param graph_ A graph object. + * @param edge_ A graph edge + * @param decoration_ A map which describes the style attributes. + */ + void decorateEdge( + util::Graph& graph_, + const util::Graph::Edge& edge_, + const Decoration& decoration_) const; + + /** + * This function decorates a graph subgraph. + * @param graph_ A graph object. + * @param subgraph_ A graph subgraph + * @param decoration_ A map which describes the style attributes. + */ + void decorateSubgraph( + util::Graph& graph_, + const util::Graph::Subgraph& subgraph_, + const Decoration& decoration_) const; + + static const Decoration centerNodeDecoration; + static const Decoration calleeNodeDecoration; + static const Decoration callerNodeDecoration; + static const Decoration calleeEdgeDecoration; + static const Decoration callerEdgeDecoration; + + std::map _subgraphs; + + std::shared_ptr _db; + util::OdbTransaction _transaction; + core::ProjectServiceHandler _projectHandler; +}; + +} +} +} + +#endif diff --git a/plugins/csharp/service/src/csharpservice.cpp b/plugins/csharp/service/src/csharpservice.cpp index 7d44b417d..8172b595f 100644 --- a/plugins/csharp/service/src/csharpservice.cpp +++ b/plugins/csharp/service/src/csharpservice.cpp @@ -4,6 +4,8 @@ #include #include +#include "csharpdiagram.h" + namespace cc { namespace service @@ -71,9 +73,8 @@ void CsharpServiceHandler::getAstNodeInfo( FileQuery::path == return_.range.file); }); std::stringstream ss; - ss << file; + ss << file->id; return_.range.file = ss.str(); - //LOG(info) << "csharpQuery.getAstNodeInfo: file = " << return_.range.file; } void CsharpServiceHandler::getAstNodeInfoByPosition( @@ -134,7 +135,7 @@ void CsharpServiceHandler::getDiagramTypes( const core::AstNodeId& astNodeId_) { LOG(info) << "getDiagramTypes"; - //csharpQueryHandler.getDiagramTypes(return_, astNodeId_); + _csharpQueryHandler.getDiagramTypes(return_, astNodeId_); } void CsharpServiceHandler::getDiagram( @@ -143,7 +144,29 @@ void CsharpServiceHandler::getDiagram( const std::int32_t diagramId_) { LOG(info) << "getDiagram"; - //csharpQueryHandler.getDiagram(return_, astNodeId_, diagramId_); + CsharpDiagram diagram(_db, _datadir, _context); + util::Graph graph; + + switch (diagramId_) + { + case FUNCTION_CALL: + { + AstNodeInfo centerInfo; + getAstNodeInfo(centerInfo, astNodeId_); + + std::vector calleeInfos; + getReferences(calleeInfos, astNodeId_, CALLEE, {}); + + std::vector callerInfos; + getReferences(callerInfos, astNodeId_, CALLER, {}); + + diagram.getFunctionCallDiagram(graph, centerInfo, calleeInfos, callerInfos); + break; + } + } + + if (graph.nodeCount() != 0) + return_ = graph.output(util::Graph::SVG); } void CsharpServiceHandler::getDiagramLegend( @@ -151,6 +174,15 @@ void CsharpServiceHandler::getDiagramLegend( const std::int32_t diagramId_) { LOG(info) << "getDiagramLegend"; + + CsharpDiagram diagram(_db, _datadir, _context); + + switch (diagramId_) + { + case FUNCTION_CALL: + return_ = diagram.getFunctionCallLegend(); + break; + } } void CsharpServiceHandler::getFileDiagramTypes( diff --git a/plugins/csharp/service/src_csharp/CSharpQueryHandler.cs b/plugins/csharp/service/src_csharp/CSharpQueryHandler.cs index a221b65b9..74ff2cc0b 100644 --- a/plugins/csharp/service/src_csharp/CSharpQueryHandler.cs +++ b/plugins/csharp/service/src_csharp/CSharpQueryHandler.cs @@ -289,7 +289,7 @@ private List queryEvents(CsharpAstNode astNode) CancellationToken cancellationToken = default(CancellationToken)) { System.Console.WriteLine("[CSharpService] getAstNodeInfoAsync"); - return await Task.FromResult(new language.AstNodeInfo()); + return await Task.FromResult(createAstNodeInfo(queryCsharpAstNode(astNodeId))); } public async Task getAstNodeInfoByPosition(string path_, @@ -664,13 +664,13 @@ public async Task getFileReferenceCount(string path, int referenceId, public async Task> getDiagramTypes(string astNodeId, CancellationToken cancellationToken = default(CancellationToken)) { - return await Task.FromResult(new Dictionary()); - } + var node = queryCsharpAstNode(astNodeId); + var ret = new Dictionary(); - public async Task getDiagram(string astNodeId, int diagramId, - CancellationToken cancellationToken = default(CancellationToken)) - { - return await Task.FromResult("Diagram"); + if (node.AstSymbolType == AstSymbolTypeEnum.Method) + ret.Add("Function call diagram", (int)DiagramType.FUNCTION_CALL); + + return await Task.FromResult(ret); } public async Task> getSyntaxHighlight(FileRange range,