/************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** Commercial Usage ** ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Nokia. ** ** GNU Lesser General Public License Usage ** ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at http://qt.nokia.com/contact. ** **************************************************************************/ #include "symbolgroup.h" #include "stringutils.h" #include "base64.h" #include #include #include #include #include enum { debug = 0 }; typedef std::vector::size_type VectorIndexType; const char rootNameC[] = "local"; enum { BufSize = 2048 }; std::ostream &operator<<(std::ostream &str, const DEBUG_SYMBOL_PARAMETERS ¶meters) { str << "parent="; if (parameters.ParentSymbol == DEBUG_ANY_ID) { str << "DEBUG_ANY_ID"; } else { str << parameters.ParentSymbol ; } str << " flags=" << parameters.Flags; // Detailed flags: if (parameters.Flags & DEBUG_SYMBOL_EXPANDED) str << " EXPANDED"; if (parameters.Flags & DEBUG_SYMBOL_READ_ONLY) str << " READONLY"; if (parameters.Flags & DEBUG_SYMBOL_IS_ARRAY) str << " ARRAY"; if (parameters.Flags & DEBUG_SYMBOL_IS_FLOAT) str << " FLOAT"; if (parameters.Flags & DEBUG_SYMBOL_IS_ARGUMENT) str << " ARGUMENT"; if (parameters.Flags & DEBUG_SYMBOL_IS_LOCAL) str << " LOCAL"; str << " typeId=" << parameters.TypeId << " subElements=" << parameters.SubElements; return str; } SymbolGroup::SymbolGroup(IDebugSymbolGroup2 *sg, const SymbolParameterVector &vec, ULONG threadId, unsigned frame) : m_symbolGroup(sg), m_threadId(threadId), m_frame(frame), m_root(SymbolGroupNode::create(sg, rootNameC, vec)) { } SymbolGroup::~SymbolGroup() { m_symbolGroup->Release(); delete m_root; } static inline bool getSymbolCount(CIDebugSymbolGroup *symbolGroup, ULONG *count, std::string *errorMessage) { const HRESULT hr = symbolGroup->GetNumberSymbols(count); if (FAILED(hr)) { *errorMessage = msgDebugEngineComFailed("GetNumberSymbols", hr); return false; } return true; } bool SymbolGroup::getSymbolParameters(CIDebugSymbolGroup *symbolGroup, SymbolParameterVector *vec, std::string *errorMessage) { ULONG count; return getSymbolCount(symbolGroup, &count, errorMessage) && getSymbolParameters(symbolGroup, 0, count, vec, errorMessage); } bool SymbolGroup::getSymbolParameters(CIDebugSymbolGroup *symbolGroup, unsigned long start, unsigned long count, SymbolParameterVector *vec, std::string *errorMessage) { if (!count) { vec->clear(); return true; } // Trim the count to the maximum count available. When expanding elements // and passing SubElements as count, SubElements might be an estimate that // is too large and triggers errors. ULONG totalCount; if (!getSymbolCount(symbolGroup, &totalCount, errorMessage)) return false; if (start >= totalCount) { std::ostringstream str; str << "SymbolGroup::getSymbolParameters: Start parameter " << start << " beyond total " << totalCount << '.'; *errorMessage = str.str(); return false; } if (start + count > totalCount) count = totalCount - start; // Get parameters. vec->resize(count); const HRESULT hr = symbolGroup->GetSymbolParameters(start, count, &(*vec->begin())); if (FAILED(hr)) { std::ostringstream str; str << "SymbolGroup::getSymbolParameters failed for index=" << start << ", count=" << count << ": " << msgDebugEngineComFailed("GetSymbolParameters", hr); *errorMessage = str.str(); return false; } return true; } SymbolGroup *SymbolGroup::create(CIDebugControl *control, CIDebugSymbols *debugSymbols, ULONG threadId, unsigned frame, std::string *errorMessage) { errorMessage->clear(); ULONG obtainedFrameCount = 0; const ULONG frameCount = frame + 1; DEBUG_STACK_FRAME *frames = new DEBUG_STACK_FRAME[frameCount]; IDebugSymbolGroup2 *idebugSymbols = 0; bool success = false; SymbolParameterVector parameters; // Obtain symbol group at stack frame. do { HRESULT hr = control->GetStackTrace(0, 0, 0, frames, frameCount, &obtainedFrameCount); if (FAILED(hr)) { *errorMessage = msgDebugEngineComFailed("GetStackTrace", hr); break; } if (obtainedFrameCount < frameCount ) { std::ostringstream str; str << "Unable to obtain frame " << frame << " (" << obtainedFrameCount << ")."; *errorMessage = str.str(); break; } hr = debugSymbols->GetScopeSymbolGroup2(DEBUG_SCOPE_GROUP_LOCALS, NULL, &idebugSymbols); if (FAILED(hr)) { *errorMessage = msgDebugEngineComFailed("GetScopeSymbolGroup2", hr); break; } hr = debugSymbols->SetScope(0, frames + frame, NULL, 0); if (FAILED(hr)) { *errorMessage = msgDebugEngineComFailed("SetScope", hr); break; } // refresh with current frame hr = debugSymbols->GetScopeSymbolGroup2(DEBUG_SCOPE_GROUP_LOCALS, idebugSymbols, &idebugSymbols); if (FAILED(hr)) { *errorMessage = msgDebugEngineComFailed("GetScopeSymbolGroup2", hr); break; } if (!SymbolGroup::getSymbolParameters(idebugSymbols, ¶meters, errorMessage)) break; success = true; } while (false); delete [] frames; if (!success) { if (idebugSymbols) idebugSymbols->Release(); return 0; } return new SymbolGroup(idebugSymbols, parameters, threadId, frame); } // ------- SymbolGroupNode SymbolGroupNode::SymbolGroupNode(CIDebugSymbolGroup *symbolGroup, const std::string &n, SymbolGroupNode *parent) : m_symbolGroup(symbolGroup), m_parent(parent), m_name(n) { memset(&m_parameters, 0, sizeof(DEBUG_SYMBOL_PARAMETERS)); m_parameters.ParentSymbol = DEBUG_ANY_ID; } void SymbolGroupNode::removeChildren() { if (!m_children.empty()) { const SymbolGroupNodePtrVectorIterator end = m_children.end(); for (SymbolGroupNodePtrVectorIterator it = m_children.begin(); it != end; ++it) delete *it; m_children.clear(); } } bool SymbolGroupNode::isArrayElement() const { return m_parent && (m_parent->m_parameters.Flags & DEBUG_SYMBOL_IS_ARRAY); } // iName: Fix array elements to be named 'array.0' instead of 'array.[0]' so // that sorting in Qt Creator works. std::string SymbolGroupNode::iName() const { std::string rc = m_name; //rc += isArrayElement() ? 'a' : 'n'; if (isArrayElement() && !rc.empty() && rc.at(0) == '[') { const std::string::size_type last = rc.size() - 1; if (rc.at(last) == ']') { rc.erase(last, 1); rc.erase(0, 1); } } return rc; } // Return full iname as 'locals.this.m_sth'. std::string SymbolGroupNode::fullIName() const { std::string rc = iName(); for (const SymbolGroupNode *p = parent(); p; p = p->parent()) { rc.insert(0, 1, '.'); rc.insert(0, p->iName()); } return rc; } // Index: Index of symbol, parameterOffset: Looking only at a part of the symbol array, offset void SymbolGroupNode::parseParameters(VectorIndexType index, VectorIndexType parameterOffset, const SymbolGroup::SymbolParameterVector &vec) { static char buf[BufSize]; ULONG obtainedSize; const bool isTopLevel = index == DEBUG_ANY_ID; if (isTopLevel) { m_parameters.Flags |= DEBUG_SYMBOL_EXPANDED; } else { m_parameters = vec.at(index - parameterOffset); if (m_parameters.SubElements == 0 || !(m_parameters.Flags & DEBUG_SYMBOL_EXPANDED)) return; // No children } if (m_parameters.SubElements > 1) m_children.reserve(m_parameters.SubElements); const VectorIndexType size = vec.size(); // Scan the top level elements const VectorIndexType startIndex = isTopLevel ? 0 : index + 1; for (VectorIndexType pos = startIndex - parameterOffset; pos < size ; pos++ ) { if (vec.at(pos).ParentSymbol == index) { const VectorIndexType symbolGroupIndex = pos + parameterOffset; HRESULT hr = m_symbolGroup->GetSymbolName(ULONG(symbolGroupIndex), buf, BufSize, &obtainedSize); const std::string name = SUCCEEDED(hr) ? std::string(buf) : std::string("unnamed"); SymbolGroupNode *child = new SymbolGroupNode(m_symbolGroup, name, this); child->parseParameters(symbolGroupIndex, parameterOffset, vec); m_children.push_back(child); } } if (isTopLevel) m_parameters.SubElements = ULONG(m_children.size()); } SymbolGroupNode *SymbolGroupNode::create(CIDebugSymbolGroup *sg, const std::string &name, const SymbolGroup::SymbolParameterVector &vec) { SymbolGroupNode *rc = new SymbolGroupNode(sg, name); rc->parseParameters(DEBUG_ANY_ID, 0, vec); return rc; } // Fix some oddities in CDB values static void fixValue(const std::string &type, std::wstring *value) { // Pointers: fix '0x00000000`00000AD class bla' ... to "0xAD", but leave // 'const char *' values as is. if (!type.empty() && type.at(type.size() - 1) == L'*' && value->compare(0, 2, L"0x") == 0) { // Remove dumb 64bit separator if (value->size() > 10 && value->at(10) == L'`') value->erase(10, 1); const std::string::size_type firstNonNullDigit = value->find_first_not_of(L"0", 2); // No on-null digits: plain null ptr. if (firstNonNullDigit == std::string::npos || value->at(firstNonNullDigit) == ' ') { *value = L"0x0"; return; } // Strip if (firstNonNullDigit > 2) value->erase(2, firstNonNullDigit - 2); // Strip ' Class bla" std::wstring::size_type classPos = value->find(L" struct", 2); if (classPos == std::string::npos) classPos = value->find(L" class", 2); if (classPos != std::string::npos) value->erase(classPos, value->size() - classPos); return; } // Integers: fix '0n10' -> '10' if (value->size() >= 3 && value->compare(0, 2, L"0n") == 0 && (isdigit(value->at(2)) || value->at(2) == L'-')) { value->erase(0, 2); return; } } // Check for ASCII-encode-able stuff. Plain characters + tabs at the most, no newline. static bool isSevenBitClean(const wchar_t *buf, ULONG size) { const wchar_t *bufEnd = buf + size; for (const wchar_t *bufPtr = buf; bufPtr < bufEnd; bufPtr++) { const wchar_t c = *bufPtr; if (c > 127 || (c < 32 && c != 9)) return false; } return true; } std::string SymbolGroupNode::getType(ULONG index) const { static char buf[BufSize]; const HRESULT hr = m_symbolGroup->GetSymbolTypeName(index, buf, BufSize, NULL); return SUCCEEDED(hr) ? std::string(buf) : std::string(); } wchar_t *SymbolGroupNode::getValue(ULONG index, ULONG *obtainedSizeIn /* = 0 */) const { // Determine size and return allocated buffer if (obtainedSizeIn) *obtainedSizeIn = 0; const ULONG maxValueSize = 262144; ULONG obtainedSize = 0; HRESULT hr = m_symbolGroup->GetSymbolValueTextWide(index, NULL, maxValueSize, &obtainedSize); if (FAILED(hr)) return 0; if (obtainedSize > maxValueSize) obtainedSize = maxValueSize; wchar_t *buffer = new wchar_t[obtainedSize]; hr = m_symbolGroup->GetSymbolValueTextWide(index, buffer, obtainedSize, &obtainedSize); if (FAILED(hr)) { // Whoops, should not happen delete [] buffer; return 0; } if (obtainedSizeIn) *obtainedSizeIn = obtainedSize; return buffer; } ULONG64 SymbolGroupNode::address(ULONG index) const { ULONG64 address = 0; const HRESULT hr = m_symbolGroup->GetSymbolOffset(index, &address); if (SUCCEEDED(hr)) return address; return 0; } std::wstring SymbolGroupNode::rawValue(ULONG index) const { std::wstring rc; if (const wchar_t *wbuf = getValue(index)) { rc = wbuf; delete[] wbuf; } return rc; } std::wstring SymbolGroupNode::fixedValue(ULONG index) const { std::wstring value = rawValue(index); fixValue(getType(index), &value); return value; } SymbolGroupNode *SymbolGroupNode::childAt(unsigned i) const { return i < m_children.size() ? m_children.at(i) : static_cast(0); } static inline void indentStream(std::ostream &str, unsigned depth) { for (unsigned d = 0; d < depth; d++) str << " "; } void SymbolGroupNode::dump(std::ostream &str, unsigned child, unsigned depth, bool humanReadable, ULONG &index) const { const std::string iname = fullIName(); const std::string type = getType(index); if (child) { // Separate list of children str << ','; if (humanReadable) str << '\n'; } if (humanReadable) indentStream(str, depth); str << "{iname=\"" << iname << "\",exp=\"" << iname << "\",name=\"" << m_name << "\",type=\"" << type << '"'; if (const ULONG64 addr = address(index)) str << ",addr=\"" << std::hex << std::showbase << addr << std::noshowbase << std::dec << '"'; ULONG obtainedSize = 0; if (const wchar_t *wbuf = getValue(index, &obtainedSize)) { const ULONG valueSize = obtainedSize - 1; // ASCII or base64? if (isSevenBitClean(wbuf, valueSize)) { std::wstring value = wbuf; fixValue(type, &value); str << ",valueencoded=\"0\",value=\"" << gdbmiWStringFormat(value) << '"'; } else { str << ",valueencoded=\"2\",value=\""; base64Encode(str, reinterpret_cast(wbuf), valueSize * sizeof(wchar_t)); str << '"'; } delete [] wbuf; } // Children: Dump all known or subelements (guess). const VectorIndexType childCountGuess = m_children.empty() ? m_parameters.SubElements : m_children.size(); // No children..suppose we are editable and enabled if (childCountGuess == 0) str << ",valueenabled=\"true\",valueeditable=\"true\""; str << ",numchild=\"" << childCountGuess << '"'; if (!m_children.empty()) { str << ",children=["; if (humanReadable) str << '\n'; } } void SymbolGroupNode::dumpChildrenVisited(std::ostream &str, bool humanReadable) const { if (!m_children.empty()) str << ']'; str << '}'; if (humanReadable) str << '\n'; } bool SymbolGroupNode::accept(SymbolGroupNodeVisitor &visitor, unsigned child, unsigned depth, ULONG &index) const { // If we happen to be the root node, just skip over const bool invisibleRoot = index == DEBUG_ANY_ID; const unsigned childDepth = invisibleRoot ? 0 : depth + 1; if (invisibleRoot) { index = 0; } else { // Visit us and move index forward. if (visitor.visit(this, child, depth, index)) return true; index++; } const unsigned childCount = unsigned(m_children.size()); for (unsigned c = 0; c < childCount; c++) if (m_children.at(c)->accept(visitor, c, childDepth, index)) return true; if (!invisibleRoot) visitor.childrenVisited(this, depth); return false; } void SymbolGroupNode::debug(std::ostream &str, unsigned verbosity, unsigned depth, ULONG index) const { indentStream(str, depth); str << '"' << m_name << "\" Children=" << m_children.size() << ' ' << m_parameters; if (verbosity) str << " Address=0x" << std::hex << address(index) << std::dec << " Type=\"" << getType(index) << "\" Value=\"" << gdbmiWStringFormat(rawValue(index)) << '"'; str << '\n'; } // Index offset when stepping past this node in a symbol parameter array. Basically // self + recursive all child counts. ULONG SymbolGroupNode::recursiveIndexOffset() const { ULONG rc = 1u; if (!m_children.empty()) { const SymbolGroupNodePtrVectorConstIterator cend = m_children.end(); for (SymbolGroupNodePtrVectorConstIterator it = m_children.begin(); it != cend; ++it) rc += (*it)->recursiveIndexOffset(); } return rc; } // Expand! bool SymbolGroupNode::expand(ULONG index, std::string *errorMessage) { if (::debug > 1) DebugPrint() << "SymbolGroupNode::expand " << m_name << ' ' << index; if (!m_children.empty()) return true; if (m_parameters.SubElements == 0) { *errorMessage = "No subelements to expand in node: " + fullIName(); return false; } const HRESULT hr = m_symbolGroup->ExpandSymbol(index, TRUE); if (FAILED(hr)) { *errorMessage = msgDebugEngineComFailed("ExpandSymbol", hr); return false; } SymbolGroup::SymbolParameterVector parameters; // Retrieve parameters (including self, re-retrieve symbol parameters to get new 'expanded' flag // and corrected SubElement count (might be estimate)) and create child nodes. if (!SymbolGroup::getSymbolParameters(m_symbolGroup, index, m_parameters.SubElements + 1, ¶meters, errorMessage)) return false; parseParameters(index, index, parameters); return true; } static inline std::string msgNotFound(const std::string &nodeName) { std::ostringstream str; str << "Node '" << nodeName << "' not found."; return str.str(); } std::string SymbolGroup::dump(bool humanReadable) const { std::ostringstream str; DumpSymbolGroupNodeVisitor visitor(str, humanReadable); if (humanReadable) str << '\n'; str << '['; accept(visitor); str << ']'; return str.str(); } // Dump a node, potentially expand std::string SymbolGroup::dump(const std::string &name, bool humanReadable, std::string *errorMessage) { ULONG index; SymbolGroupNode *const node = find(name, &index); if (node == 0) { *errorMessage = msgNotFound(name); return std::string(); } if (node->subElements() && node->children().empty()) { if (!expand(name, errorMessage)) return false; } std::ostringstream str; if (humanReadable) str << '\n'; DumpSymbolGroupNodeVisitor visitor(str, humanReadable); str << '['; node->accept(visitor, 0, 0, index); str << ']'; return str.str(); } std::string SymbolGroup::debug(unsigned verbosity) const { std::ostringstream str; DebugSymbolGroupNodeVisitor visitor(str, verbosity); accept(visitor); return str.str(); } /* expandList: Expand a list of inames with a 'mkdir -p'-like behaviour, that is, * expand all sub-paths. The list of inames has thus to be reordered to expand the * parent items first, for example "locals.this.i1.data,locals.this.i2" --->: * "locals, locals.this, locals.this.i1, locals.this.i2, locals.this.i1.data". * This is done here by creating a set of name parts keyed by level and name * (thus purging duplicates). */ typedef std::pair InamePathEntry; struct InamePathEntryLessThan : public std::binary_function { bool operator()(const InamePathEntry &i1, const InamePathEntry& i2) const { if (i1.first < i2.first) return true; if (i1.first != i2.first) return false; return i1.second < i2.second; } }; typedef std::set InamePathEntrySet; // Expand a node list "locals.i1,locals.i2" unsigned SymbolGroup::expandList(const std::vector &nodes, std::string *errorMessage) { if (nodes.empty()) return 0; // Create a set with a key . Also required for 1 node. InamePathEntrySet pathEntries; const VectorIndexType nodeCount = nodes.size(); for (VectorIndexType i= 0; i < nodeCount; i++) { const std::string &iname = nodes.at(i); std::string::size_type pos = 0; for (unsigned level = 0; pos < iname.size(); level++) { std::string::size_type dotPos = iname.find('.', pos); if (dotPos == std::string::npos) dotPos = iname.size(); pathEntries.insert(InamePathEntry(level, iname.substr(0, dotPos))); pos = dotPos + 1; } } // Now expand going by level. unsigned succeeded = 0; std::string nodeError; InamePathEntrySet::const_iterator cend = pathEntries.end(); for (InamePathEntrySet::const_iterator it = pathEntries.begin(); it != cend; ++it) if (expand(it->second, &nodeError)) { succeeded++; } else { if (!errorMessage->empty()) errorMessage->append(", "); errorMessage->append(nodeError); } return succeeded; } bool SymbolGroup::expand(const std::string &nodeName, std::string *errorMessage) { ULONG index = DEBUG_ANY_ID; SymbolGroupNode *node = find(nodeName, &index); if (::debug) DebugPrint() << "expand: " << nodeName << " found=" << (node != 0) << " index= " << index << '\n'; if (!node) { *errorMessage = msgNotFound(nodeName); return false; } if (node == m_root) // Shouldn't happen, still, all happy return true; return node->expand(index, errorMessage); } static inline std::string msgAssignError(const std::string &nodeName, const std::string &value, const std::string &why) { std::ostringstream str; str << "Unable to assign '" << value << "' to '" << nodeName << "': " << why; return str.str(); } bool SymbolGroup::assign(const std::string &nodeName, const std::string &value, std::string *errorMessage) { ULONG index; SymbolGroupNode *node = find(nodeName, &index); if (node == 0) { *errorMessage = msgAssignError(nodeName, value, "No such node"); return false; } const HRESULT hr = m_symbolGroup->WriteSymbol(index, const_cast(value.c_str())); if (FAILED(hr)) { *errorMessage = msgAssignError(nodeName, value, msgDebugEngineComFailed("WriteSymbol", hr)); return false; } return true; } bool SymbolGroup::accept(SymbolGroupNodeVisitor &visitor) const { if (!m_root || m_root->children().empty()) return false; ULONG index = DEBUG_ANY_ID; return m_root->accept(visitor, 0, 0, index); } // Find "locals.this.i1" and move index recursively static SymbolGroupNode *findNodeRecursion(const std::vector &iname, unsigned depth, std::vector nodes, ULONG *index = 0) { typedef std::vector::const_iterator ConstIt; if (::debug > 1) DebugPrint() << "findNodeRecursion " << iname.size() << '/' << iname.back() << " depth=" << depth << " nodes=" << nodes.size() << " index=" << (index ? *index : ULONG(0)); if (nodes.empty()) return 0; // Find the child that matches the iname part at depth const ConstIt cend = nodes.end(); for (ConstIt it = nodes.begin(); it != cend; ++it) { SymbolGroupNode *c = *it; if (c->name() == iname.at(depth)) { if (depth == iname.size() - 1) { // Complete iname matched->happy. return c; } else { // Sub-part of iname matched. Forward index and check children. if (index) (*index)++; // Skip ourselves return findNodeRecursion(iname, depth + 1, c->children(), index); } } else { if (index) // No match for this child, forward the index past all expanded children *index += c->recursiveIndexOffset(); } } return 0; } SymbolGroupNode *SymbolGroup::findI(const std::string &iname, ULONG *index) const { if (index) *index = DEBUG_ANY_ID; if (iname.empty()) return 0; // Match the root element only: Shouldn't happen, still, all happy if (iname == m_root->name()) return m_root; std::vector inameTokens; split(iname, '.', std::back_inserter(inameTokens)); // Must begin with root if (inameTokens.front() != m_root->name()) return 0; // Start with index = 0 at root's children if (index) *index = 0; return findNodeRecursion(inameTokens, 1, m_root->children(), index); } SymbolGroupNode *SymbolGroup::find(const std::string &iname, ULONG *index) const { SymbolGroupNode *rc = findI(iname, index); if (::debug > 1) DebugPrint() << "SymbolGroup::find " << iname << ' ' << rc << ' ' << (index ? *index : ULONG(0)); return rc; } // --------- DebugSymbolGroupNodeVisitor DebugSymbolGroupNodeVisitor::DebugSymbolGroupNodeVisitor(std::ostream &os, unsigned verbosity) : m_os(os), m_verbosity(verbosity) { } bool DebugSymbolGroupNodeVisitor::visit(const SymbolGroupNode *node, unsigned /* child */, unsigned depth, ULONG index) { node->debug(m_os, m_verbosity, depth, index); return false; } // --------------------- DumpSymbolGroupNodeVisitor DumpSymbolGroupNodeVisitor::DumpSymbolGroupNodeVisitor(std::ostream &os, bool humanReadable) : m_os(os), m_humanReadable(humanReadable) { } bool DumpSymbolGroupNodeVisitor::visit(const SymbolGroupNode *node, unsigned child, unsigned depth, ULONG index) { node->dump(m_os, child, depth, m_humanReadable, index); return false; } void DumpSymbolGroupNodeVisitor::childrenVisited(const SymbolGroupNode *node, unsigned) { node->dumpChildrenVisited(m_os, m_humanReadable); }