data: compare signatures when storing overloaded functions and methods
Function and method nodes are now distincted by their signature when added to the graph. A different node for each overloaded declaration is created with the signature as member field. fortune cookie message = Deine harte Arbeit wird schnell belohnt werden.
This commit is contained in:
@@ -119,7 +119,11 @@ void Storage::onFunctionParsed(
|
||||
{
|
||||
log("function", fullName, location);
|
||||
|
||||
Node* node = m_graph.createNodeHierarchy(fullName); // Todo: compare signatures in case of overloading.
|
||||
Node* node = m_graph.createNodeHierarchyWithDistinctSignature(
|
||||
fullName,
|
||||
ParserClient::functionSignatureStr(returnType, fullName, parameters, false)
|
||||
);
|
||||
|
||||
node->setType(Node::NODE_FUNCTION);
|
||||
|
||||
Edge* returnTypeEdge = m_graph.createEdge(
|
||||
@@ -130,12 +134,22 @@ void Storage::onFunctionParsed(
|
||||
for (const ParseVariable& parameter : parameters)
|
||||
{
|
||||
Edge* parameterEdge = m_graph.createEdge(
|
||||
Edge::EDGE_PARAMETER_OF, node, m_graph.createNodeHierarchy(parameter.type.getRawTypeName()));
|
||||
Edge::EDGE_PARAMETER_TYPE_OF, node, m_graph.createNodeHierarchy(parameter.type.getRawTypeName()));
|
||||
parameterEdge->addComponent(std::make_shared<EdgeComponentDataType>(
|
||||
parameter.type.getQualifierList(), parameter.type.getModifierStack()));
|
||||
}
|
||||
|
||||
addTokenLocation(node, location);
|
||||
|
||||
// TODO: move this into suitable TestSuite
|
||||
if (node->getSignature() != ParserClient::functionSignatureStr(returnType, fullName, parameters, false))
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Parsed and saved signatures don't match: ";
|
||||
ss << node->getSignature() << " - ";
|
||||
ss << ParserClient::functionSignatureStr(returnType, fullName, parameters, false);
|
||||
LOG_ERROR(ss.str());
|
||||
}
|
||||
}
|
||||
|
||||
void Storage::onMethodParsed(
|
||||
@@ -146,7 +160,11 @@ void Storage::onMethodParsed(
|
||||
{
|
||||
log("method", fullName, location);
|
||||
|
||||
Node* node = m_graph.createNodeHierarchy(fullName); // Todo: compare signatures in case of overloading.
|
||||
Node* node = m_graph.createNodeHierarchyWithDistinctSignature(
|
||||
fullName,
|
||||
ParserClient::functionSignatureStr(returnType, fullName, parameters, isConst)
|
||||
);
|
||||
|
||||
node->setType(Node::NODE_METHOD);
|
||||
node->setConst(isConst);
|
||||
node->setStatic(isStatic);
|
||||
@@ -166,12 +184,22 @@ void Storage::onMethodParsed(
|
||||
for (const ParseVariable& parameter : parameters)
|
||||
{
|
||||
Edge* parameterEdge = m_graph.createEdge(
|
||||
Edge::EDGE_PARAMETER_OF, node, m_graph.createNodeHierarchy(parameter.type.getRawTypeName()));
|
||||
Edge::EDGE_PARAMETER_TYPE_OF, node, m_graph.createNodeHierarchy(parameter.type.getRawTypeName()));
|
||||
parameterEdge->addComponent(std::make_shared<EdgeComponentDataType>(
|
||||
parameter.type.getQualifierList(), parameter.type.getModifierStack()));
|
||||
}
|
||||
|
||||
addTokenLocation(node, location);
|
||||
|
||||
// TODO: move this into suitable TestSuite
|
||||
if (node->getSignature() != ParserClient::functionSignatureStr(returnType, fullName, parameters, isConst))
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Parsed and saved signatures don't match: ";
|
||||
ss << node->getSignature() << " - ";
|
||||
ss << ParserClient::functionSignatureStr(returnType, fullName, parameters, isConst);
|
||||
LOG_ERROR(ss.str());
|
||||
}
|
||||
}
|
||||
|
||||
void Storage::onNamespaceParsed(const ParseLocation& location, const std::string& fullName)
|
||||
|
||||
@@ -97,7 +97,7 @@ std::string Edge::getTypeString() const
|
||||
return "is type of";
|
||||
case EDGE_RETURN_TYPE_OF:
|
||||
return "has return type";
|
||||
case EDGE_PARAMETER_OF:
|
||||
case EDGE_PARAMETER_TYPE_OF:
|
||||
return "has parameter of type";
|
||||
case EDGE_INHERITANCE:
|
||||
return "is derived from";
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
EDGE_MEMBER,
|
||||
EDGE_TYPE_OF,
|
||||
EDGE_RETURN_TYPE_OF,
|
||||
EDGE_PARAMETER_OF,
|
||||
EDGE_PARAMETER_TYPE_OF,
|
||||
EDGE_USAGE,
|
||||
EDGE_CALL,
|
||||
EDGE_INHERITANCE
|
||||
|
||||
@@ -74,6 +74,27 @@ Node* Graph::createNodeHierarchy(const std::string& fullName)
|
||||
return insertNodeHierarchy(fullName);
|
||||
}
|
||||
|
||||
Node* Graph::createNodeHierarchyWithDistinctSignature(const std::string& fullName, const std::string& signature)
|
||||
{
|
||||
Node* node = getNode(fullName);
|
||||
if (node)
|
||||
{
|
||||
if (node->getSignature() == signature)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
node = insertNode(fullName, node->getParentNode());
|
||||
}
|
||||
else
|
||||
{
|
||||
node = insertNodeHierarchy(fullName);
|
||||
}
|
||||
|
||||
node->setSignature(signature);
|
||||
return node;
|
||||
}
|
||||
|
||||
Edge* Graph::createEdge(Edge::EdgeType type, Node* from, Node* to)
|
||||
{
|
||||
Edge* edge = getEdge(type, from, to);
|
||||
@@ -288,14 +309,7 @@ Node* Graph::insertNodeHierarchy(const std::string& fullName)
|
||||
|
||||
if (!childNode)
|
||||
{
|
||||
std::shared_ptr<Node> childNodePtr = std::make_shared<Node>(Node::NODE_UNDEFINED, name);
|
||||
m_nodes.push_back(childNodePtr);
|
||||
childNode = childNodePtr.get();
|
||||
|
||||
if (node)
|
||||
{
|
||||
createEdge(Edge::EDGE_MEMBER, node, childNode);
|
||||
}
|
||||
childNode = insertNode(name, node);
|
||||
}
|
||||
|
||||
node = childNode;
|
||||
@@ -304,6 +318,21 @@ Node* Graph::insertNodeHierarchy(const std::string& fullName)
|
||||
return node;
|
||||
}
|
||||
|
||||
Node* Graph::insertNode(const std::string& fullName, Node* parentNode)
|
||||
{
|
||||
std::shared_ptr<Node> nodePtr = std::make_shared<Node>(Node::NODE_UNDEFINED, fullName);
|
||||
m_nodes.push_back(nodePtr);
|
||||
|
||||
Node* node = nodePtr.get();
|
||||
|
||||
if (parentNode)
|
||||
{
|
||||
createEdge(Edge::EDGE_MEMBER, parentNode, node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
Edge* Graph::insertEdge(Edge::EdgeType type, Node* from, Node* to)
|
||||
{
|
||||
std::shared_ptr<Edge> edgePtr = std::make_shared<Edge>(type, from, to);
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
Token* getTokenById(Id id) const;
|
||||
|
||||
Node* createNodeHierarchy(const std::string& fullName);
|
||||
Node* createNodeHierarchyWithDistinctSignature(const std::string& fullName, const std::string& signature);
|
||||
Edge* createEdge(Edge::EdgeType type, Node* from, Node* to);
|
||||
|
||||
void removeNode(Node* node);
|
||||
@@ -48,6 +49,7 @@ private:
|
||||
static const std::string DELIMITER;
|
||||
|
||||
Node* insertNodeHierarchy(const std::string& fullName);
|
||||
Node* insertNode(const std::string& fullName, Node* parentNode);
|
||||
Edge* insertEdge(Edge::EdgeType type, Node* from, Node* to);
|
||||
void removeEdgeInternal(Edge* edge);
|
||||
|
||||
|
||||
+67
-11
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <sstream>
|
||||
|
||||
// #include "data/graph/edgeComponent/EdgeComponentDataType.h"
|
||||
// #include "data/type/DataType.h"
|
||||
#include "utility/logging/logging.h"
|
||||
|
||||
Node::Node(NodeType type, const std::string& name)
|
||||
@@ -62,24 +64,24 @@ void Node::removeEdge(Edge* edge)
|
||||
m_edges.erase(find(m_edges.begin(), m_edges.end(), edge));
|
||||
}
|
||||
|
||||
Node* Node::getParentNode() const
|
||||
{
|
||||
Edge* edge = getMemberEdge();
|
||||
if (edge)
|
||||
{
|
||||
return edge->getFrom();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Edge* Node::getMemberEdge() const
|
||||
{
|
||||
Edge* edge = findEdgeOfType(Edge::EDGE_MEMBER,
|
||||
return findEdgeOfType(Edge::EDGE_MEMBER,
|
||||
[this](Edge* e)
|
||||
{
|
||||
return e->getTo() == this;
|
||||
}
|
||||
);
|
||||
|
||||
if (edge)
|
||||
{
|
||||
return edge;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_WARNING("Child edge was not found, node " + getName() + " has no parent node.");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Edge* Node::findEdge(std::function<bool(Edge*)> func) const
|
||||
@@ -94,6 +96,11 @@ Edge* Node::findEdge(std::function<bool(Edge*)> func) const
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Edge* Node::findEdgeOfType(Edge::EdgeType type) const
|
||||
{
|
||||
return findEdgeOfType(type, [](Edge* e){ return true; });
|
||||
}
|
||||
|
||||
Edge* Node::findEdgeOfType(Edge::EdgeType type, std::function<bool(Edge*)> func) const
|
||||
{
|
||||
std::vector<Edge*>::const_iterator it = find_if(m_edges.begin(), m_edges.end(),
|
||||
@@ -188,6 +195,55 @@ void Node::setStatic(bool isStatic)
|
||||
m_isStatic = isStatic;
|
||||
}
|
||||
|
||||
std::string Node::getSignature() const
|
||||
{
|
||||
// Signature generation from edges failed, because parameter edges to same type are bundled.
|
||||
// std::string str;
|
||||
|
||||
// Edge* returnTypeEdge = findEdgeOfType(Edge::EDGE_RETURN_TYPE_OF);
|
||||
// str += returnTypeEdge->getComponent<EdgeComponentDataType>()->getDataType().getFullTypeName() + " " + m_name + "(";
|
||||
|
||||
// forEachEdgeOfType(Edge::EDGE_PARAMETER_TYPE_OF,
|
||||
// [&str](Edge* edge)
|
||||
// {
|
||||
// str += edge->getComponent<EdgeComponentDataType>()->getDataType().getFullTypeName() + ", ";
|
||||
// }
|
||||
// );
|
||||
|
||||
// if (*str.rbegin() == ' ')
|
||||
// {
|
||||
// str.pop_back();
|
||||
// str.pop_back();
|
||||
// }
|
||||
// str += ")";
|
||||
|
||||
// if (isConst())
|
||||
// {
|
||||
// str += " const";
|
||||
// }
|
||||
|
||||
// return str;
|
||||
|
||||
return m_signature;
|
||||
}
|
||||
|
||||
void Node::setSignature(const std::string& signature)
|
||||
{
|
||||
if (m_type != NODE_FUNCTION && m_type != NODE_METHOD && m_type != NODE_UNDEFINED)
|
||||
{
|
||||
LOG_ERROR("Signature is not supported on node of type " + getTypeString(m_type));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_signature.size())
|
||||
{
|
||||
LOG_ERROR("Signature was already set before.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_signature = signature;
|
||||
}
|
||||
|
||||
std::string Node::getTypeString(NodeType type) const
|
||||
{
|
||||
switch (type)
|
||||
|
||||
@@ -39,9 +39,11 @@ public:
|
||||
void addEdge(Edge* edge);
|
||||
void removeEdge(Edge* edge);
|
||||
|
||||
Node* getParentNode() const;
|
||||
Edge* getMemberEdge() const;
|
||||
|
||||
Edge* findEdge(std::function<bool(Edge*)> func) const;
|
||||
Edge* findEdgeOfType(Edge::EdgeType type) const;
|
||||
Edge* findEdgeOfType(Edge::EdgeType type, std::function<bool(Edge*)> func) const;
|
||||
|
||||
void forEachEdge(std::function<void(Edge*)> func) const;
|
||||
@@ -60,6 +62,9 @@ public:
|
||||
bool isStatic() const;
|
||||
void setStatic(bool isStatic);
|
||||
|
||||
std::string getSignature() const;
|
||||
void setSignature(const std::string& signature);
|
||||
|
||||
// Logging.
|
||||
std::string getTypeString(NodeType type) const;
|
||||
std::string getAsString() const;
|
||||
@@ -76,6 +81,8 @@ private:
|
||||
// Additional fields for different NodeTypes.
|
||||
bool m_isConst;
|
||||
bool m_isStatic;
|
||||
|
||||
std::string m_signature;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostream, const Node& node);
|
||||
|
||||
@@ -1,5 +1,111 @@
|
||||
#include "data/parser/ParserClient.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "data/parser/ParseLocation.h"
|
||||
#include "data/parser/ParseVariable.h"
|
||||
#include "data/type/DataType.h"
|
||||
|
||||
std::string ParserClient::addAccessPrefix(const std::string& str, AccessType access)
|
||||
{
|
||||
switch (access)
|
||||
{
|
||||
case ACCESS_PUBLIC:
|
||||
return "public " + str;
|
||||
case ACCESS_PROTECTED:
|
||||
return "protected " + str;
|
||||
case ACCESS_PRIVATE:
|
||||
return "private " + str;
|
||||
case ACCESS_NONE:
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ParserClient::addAbstractionPrefix(const std::string& str, AbstractionType abstraction)
|
||||
{
|
||||
switch (abstraction)
|
||||
{
|
||||
case ABSTRACTION_VIRTUAL:
|
||||
return "virtual " + str;
|
||||
case ABSTRACTION_PURE_VIRTUAL:
|
||||
return "pure virtual " + str;
|
||||
case ABSTRACTION_NONE:
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ParserClient::addStaticPrefix(const std::string& str, bool isStatic)
|
||||
{
|
||||
if (isStatic)
|
||||
{
|
||||
return "static " + str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string ParserClient::addConstPrefix(const std::string& str, bool isConst, bool atFront)
|
||||
{
|
||||
if (isConst)
|
||||
{
|
||||
return atFront ? "const " + str : str + " const";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string ParserClient::addLocationSuffix(const std::string& str, const ParseLocation& location)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << str << " <" << location.startLineNumber << ":" << location.startColumnNumber << " ";
|
||||
ss << location.endLineNumber << ":" << location.endColumnNumber << ">";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string ParserClient::variableStr(const ParseVariable& variable, bool withName)
|
||||
{
|
||||
std::string str = variable.type.getFullTypeName();
|
||||
if (withName)
|
||||
{
|
||||
str += " " + variable.fullName;
|
||||
}
|
||||
return addStaticPrefix(str, variable.isStatic);
|
||||
}
|
||||
|
||||
std::string ParserClient::parameterStr(const std::vector<ParseVariable> parameters, bool withName)
|
||||
{
|
||||
std::string str = "(";
|
||||
for (size_t i = 0; i < parameters.size(); i++)
|
||||
{
|
||||
str += variableStr(parameters[i], withName);
|
||||
if (i < parameters.size() - 1)
|
||||
{
|
||||
str += ", ";
|
||||
}
|
||||
}
|
||||
return str + ")";
|
||||
}
|
||||
|
||||
std::string ParserClient::functionStr(
|
||||
const DataType& returnType,
|
||||
const std::string& fullName,
|
||||
const std::vector<ParseVariable>& parameters,
|
||||
bool isConst
|
||||
){
|
||||
return addConstPrefix(
|
||||
returnType.getFullTypeName() + " " + fullName + parameterStr(parameters),
|
||||
isConst,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
std::string ParserClient::functionSignatureStr(
|
||||
const DataType& returnType,
|
||||
const std::string& fullName,
|
||||
const std::vector<ParseVariable>& parameters,
|
||||
bool isConst
|
||||
){
|
||||
return addConstPrefix(fullName + parameterStr(parameters, false), isConst, false);
|
||||
}
|
||||
|
||||
ParserClient::ParserClient()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -24,6 +24,26 @@ public:
|
||||
ABSTRACTION_NONE
|
||||
};
|
||||
|
||||
static std::string addAccessPrefix(const std::string& str, AccessType access);
|
||||
static std::string addAbstractionPrefix(const std::string& str, AbstractionType abstraction);
|
||||
static std::string addStaticPrefix(const std::string& str, bool isStatic);
|
||||
static std::string addConstPrefix(const std::string& str, bool isConst, bool atFront);
|
||||
static std::string addLocationSuffix(const std::string& str, const ParseLocation& location);
|
||||
static std::string variableStr(const ParseVariable& variable, bool withName = true);
|
||||
static std::string parameterStr(const std::vector<ParseVariable> parameters, bool withName = true);
|
||||
static std::string functionStr(
|
||||
const DataType& returnType,
|
||||
const std::string& fullName,
|
||||
const std::vector<ParseVariable>& parameters,
|
||||
bool isConst
|
||||
);
|
||||
static std::string functionSignatureStr(
|
||||
const DataType& returnType,
|
||||
const std::string& fullName,
|
||||
const std::vector<ParseVariable>& parameters,
|
||||
bool isConst
|
||||
);
|
||||
|
||||
ParserClient();
|
||||
virtual ~ParserClient();
|
||||
|
||||
|
||||
@@ -732,9 +732,8 @@ private:
|
||||
virtual void onFunctionParsed(
|
||||
const ParseLocation& location, const std::string& fullName, const DataType& returnType,
|
||||
const std::vector<ParseVariable>& parameters
|
||||
)
|
||||
{
|
||||
std::string str = returnType.getFullTypeName() + " " + fullName + parameterStr(parameters);
|
||||
){
|
||||
std::string str = functionStr(returnType, fullName, parameters, false);
|
||||
functions.push_back(addLocationSuffix(str, location));
|
||||
}
|
||||
|
||||
@@ -744,9 +743,9 @@ private:
|
||||
bool isConst, bool isStatic
|
||||
)
|
||||
{
|
||||
std::string str = returnType.getFullTypeName() + " " + fullName + parameterStr(parameters);
|
||||
std::string str = functionStr(returnType, fullName, parameters, isConst);
|
||||
str = addStaticPrefix(addAbstractionPrefix(str, abstraction), isStatic);
|
||||
str = addConstPrefix(addAccessPrefix(str, access), isConst, false);
|
||||
str = addAccessPrefix(str, access);
|
||||
str = addLocationSuffix(str, location);
|
||||
methods.push_back(str);
|
||||
}
|
||||
@@ -791,81 +790,6 @@ private:
|
||||
std::vector<std::string> structs;
|
||||
std::vector<std::string> inheritances;
|
||||
std::vector<std::string> calls;
|
||||
|
||||
private:
|
||||
std::string addAccessPrefix(const std::string& str, AccessType access)
|
||||
{
|
||||
switch (access)
|
||||
{
|
||||
case ACCESS_PUBLIC:
|
||||
return "public " + str;
|
||||
case ACCESS_PROTECTED:
|
||||
return "protected " + str;
|
||||
case ACCESS_PRIVATE:
|
||||
return "private " + str;
|
||||
case ACCESS_NONE:
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
std::string addAbstractionPrefix(const std::string& str, AbstractionType abstraction)
|
||||
{
|
||||
switch (abstraction)
|
||||
{
|
||||
case ABSTRACTION_VIRTUAL:
|
||||
return "virtual " + str;
|
||||
case ABSTRACTION_PURE_VIRTUAL:
|
||||
return "pure virtual " + str;
|
||||
case ABSTRACTION_NONE:
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
std::string addStaticPrefix(const std::string& str, bool isStatic)
|
||||
{
|
||||
if (isStatic)
|
||||
{
|
||||
return "static " + str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string addConstPrefix(const std::string& str, bool isConst, bool atFront)
|
||||
{
|
||||
if (isConst)
|
||||
{
|
||||
return atFront ? "const " + str : str + " const";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string variableStr(const ParseVariable& variable)
|
||||
{
|
||||
std::string str = variable.type.getFullTypeName() + " " + variable.fullName;
|
||||
return addStaticPrefix(str, variable.isStatic);
|
||||
}
|
||||
|
||||
std::string parameterStr(const std::vector<ParseVariable> parameters)
|
||||
{
|
||||
std::string str = "(";
|
||||
for (size_t i = 0; i < parameters.size(); i++)
|
||||
{
|
||||
str += variableStr(parameters[i]);
|
||||
if (i < parameters.size() - 1)
|
||||
{
|
||||
str += ", ";
|
||||
}
|
||||
}
|
||||
return str + ")";
|
||||
}
|
||||
|
||||
std::string addLocationSuffix(const std::string& str, const ParseLocation& location)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << str << " <" << location.startLineNumber << ":" << location.startColumnNumber << " ";
|
||||
ss << location.endLineNumber << ":" << location.endColumnNumber << ">";
|
||||
return ss.str();
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<TestParserClient> parseCode(std::string code) const
|
||||
|
||||
@@ -105,7 +105,12 @@ public:
|
||||
TS_ASSERT_EQUALS(2, graph.getNodeCount());
|
||||
TS_ASSERT_EQUALS(1, graph.getEdgeCount());
|
||||
|
||||
TS_ASSERT(ab->getMemberEdge());
|
||||
TS_ASSERT(graph.getEdge(Edge::EDGE_MEMBER, a, ab));
|
||||
TS_ASSERT_EQUALS(ab->getMemberEdge(), graph.getEdge(Edge::EDGE_MEMBER, a, ab));
|
||||
TS_ASSERT_EQUALS(ab->getMemberEdge()->getFrom(), a);
|
||||
TS_ASSERT_EQUALS(ab->getMemberEdge()->getTo(), ab);
|
||||
TS_ASSERT_EQUALS(ab->getParentNode(), a);
|
||||
}
|
||||
|
||||
void test_graph_removes_nodes()
|
||||
@@ -161,7 +166,7 @@ public:
|
||||
TS_ASSERT(graph.getNode("A::C"));
|
||||
}
|
||||
|
||||
void test_graph_creates_multiple_nodes_as_type_nodes()
|
||||
void test_graph_creates_multiple_nodes_as_undefined_nodes()
|
||||
{
|
||||
TestGraph graph;
|
||||
Node* abc = graph.createNodeHierarchy("A::B::C");
|
||||
|
||||
Reference in New Issue
Block a user