diff --git a/doc_internal/Doxyfile.in b/doc_internal/Doxyfile.in index 554ff2b9759..a31680fdf81 100644 --- a/doc_internal/Doxyfile.in +++ b/doc_internal/Doxyfile.in @@ -323,6 +323,11 @@ ALLEXTERNALS = NO EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- +# Configuration options related to the requirements traceability +#--------------------------------------------------------------------------- +REQUIREMENTS_FILES = +REQUIREMENTS_TAGFILES = +#--------------------------------------------------------------------------- # Configuration options related to diagram generator tools #--------------------------------------------------------------------------- HIDE_UNDOC_RELATIONS = YES diff --git a/doc_internal/doxygen.md b/doc_internal/doxygen.md index 979f101a3aa..934638197ac 100644 --- a/doc_internal/doxygen.md +++ b/doc_internal/doxygen.md @@ -188,6 +188,20 @@ following values (each option has to be preceded by `-d`): - `lex:lexer`
Enables output for a specific lexer only, where `lexer` should be replaced by the name of the specific lexer. + +Producing Requirements Traceability +=================================== + +In order to produce 2 way requirements traceability tables between requirements and code follow the following workflow: + - Create Requirements by either: + - Importing external requirements using the REQUIREMENTS_TAGFILES configuration option (see TAGFILES for expected usage) + - Create requirements files by specifying them in the REQUIREMENTS_FILES configuration option. + Requirements will be extracted from anchors in the files or tagfiles where the anchor name match (PREFIX)(NUMBER) where PREFIX can be zero or more characters. Regex ("^(.*?)(\\d+)$"), + It is expected that the prefix is unique for a processed requirements file to facilitate the traceability. + - In the code specify which classes or methods satisfies a requirement using the @satisfies keyword with the requirement as the parameter. e.g. @satisfies REQ_1234 + - In the code specify which test verifies a requirement using the @satisfies keyword with the requirement as the parameter. e.g. @verifies REQ_1234 + + Producing output ================ diff --git a/doc_internal/tags_history.md b/doc_internal/tags_history.md old mode 100755 new mode 100644 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7dc358d0cd8..42bbf1eb883 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -298,6 +298,7 @@ add_library(doxymain STATIC qhp.cpp reflist.cpp regex.cpp + requirementstracker.cpp resourcemgr.cpp rtfdocvisitor.cpp rtfgen.cpp diff --git a/src/cmdmapper.cpp b/src/cmdmapper.cpp index 82f24278d18..a92c4ab9a92 100644 --- a/src/cmdmapper.cpp +++ b/src/cmdmapper.cpp @@ -159,6 +159,8 @@ static const CommandMap g_cmdMap = { "endiliteral", CommandType::CMD_ENDILITERAL }, { "ianchor" , CommandType::CMD_IANCHOR }, { "iprefix" , CommandType::CMD_IPREFIX }, + { "satisfies", CommandType::CMD_SATISFIES }, + { "verifies", CommandType::CMD_VERIFIES }, }; //---------------------------------------------------------------------------- diff --git a/src/cmdmapper.h b/src/cmdmapper.h index 6058693c9ab..b56448d5645 100644 --- a/src/cmdmapper.h +++ b/src/cmdmapper.h @@ -162,7 +162,9 @@ enum class CommandType CMD_IPREFIX = 130, CMD_PLANTUMLFILE = 131, CMD_EXCLAMATION = 132, - CMD_QUESTION = 133 + CMD_QUESTION = 133, + CMD_SATISFIES = 134 | SIMPLESECT_BIT, + CMD_VERIFIES = 135 | SIMPLESECT_BIT }; enum class HtmlTagType diff --git a/src/commentscan.l b/src/commentscan.l index a9ae84d6af3..e38f06daab7 100644 --- a/src/commentscan.l +++ b/src/commentscan.l @@ -174,6 +174,8 @@ static bool handleIFile(yyscan_t yyscanner,const QCString &, const StringVector static bool handleILine(yyscan_t yyscanner,const QCString &, const StringVector &); static bool handleIRaise(yyscan_t yyscanner,const QCString &, const StringVector &); static bool handleIPrefix(yyscan_t yyscanner,const QCString &, const StringVector &); +static bool handleSatisfies(yyscan_t yyscanner,const QCString &, const StringVector &); +static bool handleVerifies(yyscan_t yyscanner,const QCString &, const StringVector &); [[maybe_unused]] static const char *stateToString(int state); @@ -339,6 +341,7 @@ static const std::map< std::string, DocCmdMap > docCmdMap = { "rtfinclude", { nullptr, CommandSpacing::Inline, SectionHandling::Break }}, { "rtfonly", { &handleFormatBlock, CommandSpacing::Invisible, SectionHandling::Break }}, { "sa", { nullptr, CommandSpacing::Block, SectionHandling::Break }}, + { "satisfies", { &handleSatisfies, CommandSpacing::Inline, SectionHandling::Replace }}, { "section", { &handleSection, CommandSpacing::Block, SectionHandling::Break }}, { "see", { nullptr, CommandSpacing::Block, SectionHandling::Break }}, { "short", { &handleBrief, CommandSpacing::Invisible, SectionHandling::Break }}, @@ -371,6 +374,7 @@ static const std::map< std::string, DocCmdMap > docCmdMap = { "verbatim", { &handleFormatBlock, CommandSpacing::Block, SectionHandling::Break }}, { "iverbatim", { &handleFormatBlock, CommandSpacing::Block, SectionHandling::Break }}, { "verbinclude", { nullptr, CommandSpacing::Inline, SectionHandling::Break }}, + { "verifies", { &handleVerifies, CommandSpacing::Inline, SectionHandling::Replace }}, { "version", { nullptr, CommandSpacing::Block, SectionHandling::Break }}, { "warning", { nullptr, CommandSpacing::Block, SectionHandling::Break }}, { "weakgroup", { &handleWeakGroup, CommandSpacing::Invisible, SectionHandling::Escape }}, @@ -696,6 +700,8 @@ STopt [^\n@\\]* %x ILineSection %x IRaise %x IRaisePrefix +%x SatisfiesLabel +%x VerifiesLabel %% @@ -2299,6 +2305,48 @@ STopt [^\n@\\]* } +{LABELID} { // found requirement ID for @satisfies + QCString reqId = yytext; + yyextra->current->satisfies.push_back(reqId); + BEGIN(Comment); + } +{DOCNL} { // missing argument for @satisfies + warn(yyextra->fileName,yyextra->lineNr, + "@satisfies command has no requirement ID" + ); + if (*yytext=='\n') yyextra->lineNr++; + addOutput(yyscanner,'\n'); + BEGIN(Comment); + } +. { // invalid character for requirement ID + warn(yyextra->fileName,yyextra->lineNr, + "Invalid or missing requirement ID for @satisfies" + ); + addOutput(yyscanner,yytext); + BEGIN(Comment); + } + +{LABELID} { // found requirement ID for @verifies + QCString reqId = yytext; + yyextra->current->verifies.push_back(reqId); + BEGIN(Comment); + } +{DOCNL} { // missing argument for @verifies + warn(yyextra->fileName,yyextra->lineNr, + "@verifies command has no requirement ID" + ); + if (*yytext=='\n') yyextra->lineNr++; + addOutput(yyscanner,'\n'); + BEGIN(Comment); + } +. { // invalid character for requirement ID + warn(yyextra->fileName,yyextra->lineNr, + "Invalid or missing requirement ID for @verifies" + ); + addOutput(yyscanner,yytext); + BEGIN(Comment); + } + /* ----- handle arguments of the preformatted block commands ------- */ {CMD}("endverbatim"|"endiverbatim"|"endiliteral"|"endlatexonly"|"endhtmlonly"|"endxmlonly"|"enddocbookonly"|"endrtfonly"|"endmanonly"|"enddot"|"endcode"|"endicode"|"endmsc")/{NW} { // possible ends @@ -3621,6 +3669,20 @@ static bool handleIPrefix(yyscan_t yyscanner,const QCString &, const StringVecto return FALSE; } +static bool handleSatisfies(yyscan_t yyscanner,const QCString &, const StringVector &optList) +{ + struct yyguts_t *yyg = (struct yyguts_t*)yyscanner; + BEGIN(SatisfiesLabel); + return FALSE; +} + +static bool handleVerifies(yyscan_t yyscanner,const QCString &, const StringVector &optList) +{ + struct yyguts_t *yyg = (struct yyguts_t*)yyscanner; + BEGIN(VerifiesLabel); + return FALSE; +} + static bool handleIf(yyscan_t yyscanner,const QCString &, const StringVector &) { struct yyguts_t *yyg = (struct yyguts_t*)yyscanner; diff --git a/src/config.xml b/src/config.xml index 8beedb85d07..652051c2129 100644 --- a/src/config.xml +++ b/src/config.xml @@ -3803,6 +3803,64 @@ where `loc1` and `loc2` can be relative or absolute paths or URLs. If the \c EXTERNAL_PAGES tag is set to \c YES, all external pages will be listed in the related pages index. If set to \c NO, only the current project's pages will be listed. +]]> + + + + diff --git a/src/docbookvisitor.cpp b/src/docbookvisitor.cpp index 3c5ea0a6d5d..a784a07b22a 100644 --- a/src/docbookvisitor.cpp +++ b/src/docbookvisitor.cpp @@ -854,6 +854,18 @@ DB_VIS_C m_t << "" << convertToDocBook(theTranslator->trImportant()) << "\n"; } break; + case DocSimpleSect::Satisfies: + if (s.hasTitle()) + m_t << "" << convertToDocBook(theTranslator->trSatisfies()) << "\n"; + else + m_t << "\n"; + break; + case DocSimpleSect::Verifies: + if (s.hasTitle()) + m_t << "" << convertToDocBook(theTranslator->trVerifies()) << "\n"; + else + m_t << "\n"; + break; case DocSimpleSect::User: case DocSimpleSect::Rcs: case DocSimpleSect::Unknown: @@ -889,6 +901,13 @@ DB_VIS_C case DocSimpleSect::Important: m_t << "\n"; break; + case DocSimpleSect::Satisfies: + case DocSimpleSect::Verifies: + if (s.hasTitle()) + m_t << "\n"; + else + m_t << "\n"; + break; case DocSimpleSect::Warning: m_t << "\n"; break; diff --git a/src/docnode.cpp b/src/docnode.cpp index ee69a6310a3..9d8fe3a7cc5 100644 --- a/src/docnode.cpp +++ b/src/docnode.cpp @@ -38,6 +38,7 @@ #include "trace.h" #include "anchor.h" #include "aliases.h" +#include "requirementstracker.h" #if !ENABLE_DOCPARSER_TRACING #undef AUTO_TRACE @@ -3211,6 +3212,8 @@ QCString DocSimpleSect::typeString() const case Important: return "important"; case User: return "user"; case Rcs: return "rcs"; + case Satisfies: return "satisfies"; + case Verifies: return "verifies"; } return "unknown"; } @@ -3721,6 +3724,208 @@ Token DocPara::handleXRefItem() return retval; } +Token DocPara::handleRequirementRef(bool isSatisfies) +{ + AUTO_TRACE(); + Token retval=parser()->tokenizer.lex(); + if (!retval.is(TokenRetval::TK_WHITESPACE)) + { + warn_doc_error(parser()->context.fileName,parser()->tokenizer.getLineNr(), + "expected whitespace after '@{}' command", + isSatisfies ? "satisfies" : "verifies"); + return retval; + } + + parser()->tokenizer.setStatePara(); + retval=parser()->tokenizer.lex(); + + QCString reqId; + if (retval.is(TokenRetval::TK_WORD) || retval.is(TokenRetval::TK_LNKWORD)) + { + reqId = parser()->context.token->name; + + // Get the qualified name of the current scope + QCString qualifiedName; + if (parser()->context.memberDef) + { + // If we're inside a member (function/method), use the member's qualified name + qualifiedName = parser()->context.memberDef->qualifiedName(); + } + else if (parser()->context.scope) + { + // Otherwise use the scope (class/namespace) + qualifiedName = parser()->context.scope->qualifiedName(); + } + else if (!parser()->context.context.isEmpty()) + { + qualifiedName = parser()->context.context; + } + + if (!qualifiedName.isEmpty() && !reqId.isEmpty()) + { + // Add the reference to the requirements tracker + RequirementsTracker &tracker = RequirementsTracker::instance(); + if (isSatisfies) + { + tracker.addSatisfiedBy(reqId, qualifiedName); + } + else + { + tracker.addVerifiedBy(reqId, qualifiedName); + } + } + + // Use the same approach as handleSimpleSection to add the requirement display + DocSimpleSect::Type sectType = isSatisfies ? DocSimpleSect::Satisfies : DocSimpleSect::Verifies; + DocSimpleSect *ss=nullptr; + bool needsSeparator = FALSE; + + // Helper function to recursively search for a SimpleSect of given type in a node list + std::function findSimpleSectInList; + findSimpleSectInList = [&](DocNodeList& nodeList) -> DocSimpleSect* { + for (auto &child : nodeList) + { + if (DocSimpleSect *sect = std::get_if(&child)) + { + if (sect->type() == sectType) + { + return sect; + } + } + // Check compound nodes that might contain SimpleSects + else if (DocPara *para = std::get_if(&child)) + { + if (DocSimpleSect *found = findSimpleSectInList(para->children())) + { + return found; + } + } + } + return nullptr; + }; + + // Traverse up to find the root node + DocNodeVariant *current = thisVariant(); + DocNodeVariant *root = current; + while (current) + { + root = current; + DocNodeVariant *nextParent = nullptr; + + std::visit([&](auto &&arg) { + using T = std::decay_t; + if constexpr (std::is_base_of_v) + { + nextParent = arg.parent(); + } + }, *current); + + if (nextParent) + { + current = nextParent; + } + else + { + break; + } + } + + // Search from the root for an existing section of the same type + if (DocRoot *docRoot = std::get_if(root)) + { + ss = findSimpleSectInList(docRoot->children()); + if (ss) + { + needsSeparator = TRUE; + } + } + else if (DocPara *docPara = std::get_if(root)) + { + ss = findSimpleSectInList(docPara->children()); + if (ss) + { + needsSeparator = TRUE; + } + } + + // If not found at root level, check immediate children + if (!ss && !children().empty() && // has previous element + (ss=children().get_last()) && // was a simple sect + ss->type()==sectType) // of same type + { + // append to previous section + needsSeparator = TRUE; + } + + if (!ss) + { + // Start new section + children().append(parser(),thisVariant(),sectType); + ss = children().get_last(); + } + + // Add separator if appending to existing section + if (needsSeparator) + { + ss->children().append(parser(),ss->thisVariant()); + } + + // Create a paragraph for the requirement reference + ss->children().append(parser(),ss->thisVariant()); + DocPara *par = ss->children().get_last(); + + // Try to get the requirement URL from the tracker + RequirementsTracker &tracker = RequirementsTracker::instance(); + RequirementsTracker::RequirementsCollection* collection = tracker.findCollectionByPrefix(reqId); + + if (collection) { + auto reqIt = collection->requirements.find(reqId); + if (reqIt != collection->requirements.end() && !reqIt->second.url.isEmpty()) { + // Create an external hyperlink to the requirement + HtmlAttribList attribs; + par->children().append(parser(),par->thisVariant(),attribs,reqIt->second.url,QCString(),QCString()); + DocHRef *href = par->children().get_last(); + href->children().append(parser(),href->thisVariant(),reqId); + } else { + // Fallback to plain text if no URL available + par->children().append(parser(),par->thisVariant(),reqId); + } + } else { + // Fallback to plain text if requirement not found + par->children().append(parser(),par->thisVariant(),reqId); + } + } + else + { + warn_doc_error(parser()->context.fileName,parser()->tokenizer.getLineNr(), + "missing requirement ID after '@{}' command", + isSatisfies ? "satisfies" : "verifies"); + parser()->tokenizer.setStatePara(); + return retval; + } + + parser()->tokenizer.setStatePara(); + + // Check if the next token is a period - consume it to avoid dangling punctuation + Token nextTok = parser()->tokenizer.lex(); + if (nextTok.is_any_of(TokenRetval::TK_WORD, TokenRetval::TK_SYMBOL)) + { + if (parser()->context.token->name != ".") + { + // Not a period, return it for normal processing + return nextTok; + } + // If it IS a period, we just consume it (don't return it) + } + else + { + // Not a word/symbol token, return it for normal processing + return nextTok; + } + + return Token::make_RetVal_OK(); +} + void DocPara::handleShowDate(char cmdChar,const QCString &cmdName) { AUTO_TRACE(); @@ -4942,6 +5147,12 @@ Token DocPara::handleCommand(char cmdChar, const QCString &cmdName) parser()->tokenizer.setStatePara(); } break; + case CommandType::CMD_SATISFIES: + retval = handleRequirementRef(true); + break; + case CommandType::CMD_VERIFIES: + retval = handleRequirementRef(false); + break; default: // we should not get here! warn_doc_error(parser()->context.fileName,parser()->tokenizer.getLineNr(),"Unexpected command '{}' in paragraph context",cmdName); diff --git a/src/docnode.h b/src/docnode.h index 64943ccde1a..3a841ffe643 100644 --- a/src/docnode.h +++ b/src/docnode.h @@ -1020,7 +1020,7 @@ class DocSimpleSect : public DocCompoundNode { Unknown, See, Return, Author, Authors, Version, Since, Date, Note, Warning, Copyright, Pre, Post, Invar, Remark, Attention, Important, - User, Rcs + User, Rcs, Satisfies, Verifies }; DocSimpleSect(DocParser *parser,DocNodeVariant *parent,Type t); Type type() const { return m_type; } @@ -1092,6 +1092,7 @@ class DocPara : public DocCompoundNode Token handleHtmlEndTag(const QCString &tagName); Token handleSimpleSection(DocSimpleSect::Type t,bool xmlContext=FALSE); Token handleXRefItem(); + Token handleRequirementRef(bool isSatisfies); Token handleParamSection(const QCString &cmdName,DocParamSect::Type t, bool xmlContext, int direction); void handleIncludeOperator(const QCString &cmdName,DocIncOperator::Type t); template void handleFile(const QCString &cmdName); diff --git a/src/doxygen.cpp b/src/doxygen.cpp index 876055b69b4..535fe16bff7 100644 --- a/src/doxygen.cpp +++ b/src/doxygen.cpp @@ -110,6 +110,7 @@ #include "moduledef.h" #include "stringutil.h" #include "singlecomment.h" +#include "requirementstracker.h" #include @@ -223,6 +224,7 @@ void clearAll() CitationManager::instance().clear(); Doxygen::mainPage.reset(); FormulaManager::instance().clear(); + RequirementsTracker::instance().reset(); } class Statistics @@ -9882,20 +9884,30 @@ static void resolveUserReferences() // generate all separate documentation pages -static void generatePageDocs() +static void generatePageDocsForList(const std::vector &pages, OutputList &ol) { - //printf("documentedPages=%d real=%d\n",documentedPages,Doxygen::pageLinkedMap->count()); - if (Index::instance().numDocumentedPages()==0) return; - for (const auto &pd : *Doxygen::pageLinkedMap) + for (const auto &pd : pages) { if (!pd->getGroupDef() && !pd->isReference()) { msg("Generating docs for page {}...\n",pd->name()); - pd->writeDocumentation(*g_outputList); + pd->writeDocumentation(ol); } } } +static void generatePageDocs() +{ + //printf("documentedPages=%d real=%d\n",documentedPages,Doxygen::pageLinkedMap->count()); + if (Index::instance().numDocumentedPages()==0) return; + std::vector pageList; + for (const auto &pd : *Doxygen::pageLinkedMap) + { + pageList.push_back(pd.get()); + } + generatePageDocsForList(pageList, *g_outputList); +} + //---------------------------------------------------------------------------- // create a (sorted) list & dictionary of example pages @@ -10587,8 +10599,6 @@ static void generateDiskNames() } } - - //---------------------------------------------------------------------------- static std::unique_ptr getParserForFile(const QCString &fn) @@ -11174,6 +11184,41 @@ void readFileOrDirectory(const QCString &s, } } +//---------------------------------------------------------------------------- +// Wrapper functions to make static parseFile functions accessible + +std::unique_ptr getParserForFileExt(const QCString &fileName) +{ + return getParserForFile(fileName); +} + +std::shared_ptr parseFileWithParser(OutlineParserInterface &parser, + FileDef *fd, + const QCString &fileName, + ClangTUParser *clangParser, + bool newTU) +{ + return parseFile(parser, fd, fileName, clangParser, newTU); +} + +void buildPageListFromEntry(Entry *root) +{ + buildPageList(root); +} + +OutputList* getGlobalOutputList() +{ + return g_outputList; +} + +void generateDocsForPages(const std::vector &pages) +{ + if (g_outputList) + { + generatePageDocsForList(pages, *g_outputList); + } +} + //---------------------------------------------------------------------------- static void dumpSymbol(TextStream &t,Definition *d) @@ -12339,6 +12384,37 @@ void searchInputFiles() } } + // Process REQUIREMENTS_FILES as special input files + const StringVector &reqFilesList=Config_getList(REQUIREMENTS_FILES); + if (!reqFilesList.empty()) + { + g_s.begin("Processing REQUIREMENTS_FILES...\n"); + for (const auto &s : reqFilesList) + { + QCString path=s.c_str(); + size_t l = path.length(); + if (l>0) + { + // strip trailing slashes + if (path.at(l-1)=='\\' || path.at(l-1)=='/') path=path.left(l-1); + + readFileOrDirectory( + path, // s + Doxygen::inputNameLinkedMap, // fnDict + &excludeNameSet, // exclSet + &Config_getList(FILE_PATTERNS), // patList + &exclPatterns, // exclPatList + &g_inputFiles, // resultList + nullptr, // resultSet + alwaysRecursive, // recursive + TRUE, // errorIfNotExist + &killSet, // killSet + &Doxygen::inputPaths); // paths + } + } + g_s.end(); + } + // Sort the FileDef objects by full path to get a predictable ordering over multiple runs std::stable_sort(Doxygen::inputNameLinkedMap->begin(), Doxygen::inputNameLinkedMap->end(), @@ -12640,6 +12716,22 @@ void parseInput() { readTagFile(root,s.c_str()); } + + // Read REQUIREMENTS_TAGFILES as regular tag files so anchors are available for \ref + msg("Reading and parsing requirements tag files\n"); + const StringVector &requirementsTagFileList = Config_getList(REQUIREMENTS_TAGFILES); + for (const auto &s : requirementsTagFileList) + { + readTagFile(root,s.c_str()); + } + + + /************************************************************************** + * Handle Requirements Tag Files * + **************************************************************************/ + + msg("Initializing requirements traceability support\n"); + RequirementsTracker::instance().initialize(); } /************************************************************************** @@ -12711,6 +12803,13 @@ void parseInput() buildFileList(root.get()); g_s.end(); + // Collect requirements traceability BEFORE any documentation is processed. + // This allows us to add sections to Entry->doc which will then be transferred to Definitions. + // This must be before buildClassList() transfers Entry->doc to ClassDef. + g_s.begin("Collecting requirements traceability information...\n"); + RequirementsTracker::instance().collectFromEntries(root); + g_s.end(); + g_s.begin("Building class list...\n"); buildClassList(root.get()); g_s.end(); @@ -13221,6 +13320,11 @@ void generateOutput() generateFileDocs(); g_s.end(); + // Generate traceability pages before regular page documentation + g_s.begin("Generating traceability pages...\n"); + RequirementsTracker::instance().generateTraceabilityPages(); + g_s.end(); + g_s.begin("Generating page documentation...\n"); generatePageDocs(); g_s.end(); @@ -13258,6 +13362,12 @@ void generateOutput() if (g_outputList->size()>0) { + if (RequirementsTracker::instance().hasRequirements()) + { + g_s.begin("Generating traceability index...\n"); + writeTraceabilityIndex(*g_outputList); + g_s.end(); + } writeIndexHierarchy(*g_outputList); } @@ -13392,11 +13502,18 @@ void generateOutput() g_s.print(); Debug::clearFlag(Debug::Time); + + // Clean up requirements traceability temporary files + RequirementsTracker::instance().finalize(); + msg("finished...\n"); Debug::setFlag(Debug::Time); } else { + // Clean up requirements traceability temporary files + RequirementsTracker::instance().finalize(); + msg("finished...\n"); } diff --git a/src/doxygen.h b/src/doxygen.h index e0c65a7c017..28c2b2951fd 100644 --- a/src/doxygen.h +++ b/src/doxygen.h @@ -47,6 +47,9 @@ class NamespaceLinkedMap; class NamespaceDef; class DirRelationLinkedMap; class IndexList; +class Entry; +class OutlineParserInterface; +class ClangTUParser; class Preprocessor; struct MemberGroupInfo; class NamespaceDefMutable; @@ -162,4 +165,13 @@ void readFileOrDirectory(const QCString &s, StringUnorderedSet *paths = nullptr ); +// Functions for processing generated files +std::unique_ptr getParserForFileExt(const QCString &fn); +std::shared_ptr parseFileWithParser(OutlineParserInterface &parser, + FileDef *fd,const QCString &fn, + ClangTUParser *clangParser,bool newTU); +void buildPageListFromEntry(Entry *root); +OutputList* getGlobalOutputList(); +void generateDocsForPages(const std::vector &pages); + #endif diff --git a/src/entry.h b/src/entry.h index 588c937cbe3..778916e147d 100644 --- a/src/entry.h +++ b/src/entry.h @@ -233,6 +233,8 @@ class Entry QCString metaData; //!< Slice metadata QCString req; //!< C++20 requires clause std::vector qualifiers; //!< qualifiers specified with the qualifier command + std::vector satisfies; //!< list of requirement IDs this code satisfies + std::vector verifies; //!< list of requirement IDs this code verifies /// return the command name used to define GROUPDOC_SEC const char *groupDocCmd() const diff --git a/src/htmldocvisitor.cpp b/src/htmldocvisitor.cpp index ba2fa4b96a5..3bdbb59599c 100644 --- a/src/htmldocvisitor.cpp +++ b/src/htmldocvisitor.cpp @@ -1426,6 +1426,10 @@ void HtmlDocVisitor::operator()(const DocSimpleSect &s) m_t << theTranslator->trAttention(); break; case DocSimpleSect::Important: m_t << theTranslator->trImportant(); break; + case DocSimpleSect::Satisfies: + m_t << theTranslator->trSatisfies(); break; + case DocSimpleSect::Verifies: + m_t << theTranslator->trVerifies(); break; case DocSimpleSect::User: break; case DocSimpleSect::Rcs: break; case DocSimpleSect::Unknown: break; diff --git a/src/htmlgen.cpp b/src/htmlgen.cpp index 74cccd1a888..8905782c36e 100644 --- a/src/htmlgen.cpp +++ b/src/htmlgen.cpp @@ -47,6 +47,7 @@ #include "ftvhelp.h" #include "resourcemgr.h" #include "tooltip.h" +#include "requirementstracker.h" #include "fileinfo.h" #include "dir.h" #include "utf8.h" @@ -2816,6 +2817,8 @@ static bool quickLinkVisible(LayoutNavEntry::Kind kind) case LayoutNavEntry::ExceptionList: return index.numAnnotatedExceptions()>0; case LayoutNavEntry::ExceptionIndex: return index.numAnnotatedExceptions()>0; case LayoutNavEntry::ExceptionHierarchy: return index.numHierarchyExceptions()>0; + case LayoutNavEntry::Requirements: return RequirementsTracker::instance().hasRequirements(); + case LayoutNavEntry::Traceability: return RequirementsTracker::instance().hasRequirements(); case LayoutNavEntry::None: // should never happen, means not properly initialized assert(kind != LayoutNavEntry::None); return FALSE; @@ -2991,6 +2994,8 @@ static void writeDefaultQuickLinks(TextStream &t, highlightParent = true; break; case HighlightedItem::FileVisible: kind = LayoutNavEntry::FileList; altKind = LayoutNavEntry::Files; highlightParent = true; break; + case HighlightedItem::Requirements: kind = LayoutNavEntry::Requirements; break; + case HighlightedItem::Traceability: kind = LayoutNavEntry::Traceability; break; case HighlightedItem::None: break; case HighlightedItem::Search: break; } diff --git a/src/index.cpp b/src/index.cpp index 9058ca0fcda..0b73f65ffe5 100644 --- a/src/index.cpp +++ b/src/index.cpp @@ -32,6 +32,7 @@ #include "util.h" #include "groupdef.h" #include "language.h" +#include "requirementstracker.h" #include "htmlgen.h" #include "htmlhelp.h" #include "ftvhelp.h" @@ -3897,6 +3898,210 @@ static void writeExampleIndex(OutputList &ol) } +//---------------------------------------------------------------------------- + +void writeRequirementsIndex(OutputList &ol) +{ + RequirementsTracker &tracker = RequirementsTracker::instance(); + if (!tracker.hasRequirements()) return; + + const auto &collections = tracker.collections(); + if (collections.empty()) return; + + ol.pushGeneratorState(); + ol.disable(OutputType::Man); + ol.disable(OutputType::Docbook); + + LayoutNavEntry *lne = LayoutDocManager::instance().rootNavEntry()->find(LayoutNavEntry::Requirements); + QCString title = lne ? lne->title() : QCString("Requirements"); + bool addToIndex = lne==nullptr || lne->visible(); + + startFile(ol,"requirements",false,QCString(),title,HighlightedItem::Requirements); + + startTitle(ol,QCString()); + ol.parseText(title); + endTitle(ol,QCString(),QCString()); + + ol.startContents(); + + if (addToIndex) + { + Doxygen::indexList->addContentsItem(TRUE,title,QCString(),"requirements",QCString(),TRUE,TRUE); + Doxygen::indexList->incContentsDepth(); + } + + ol.startTextBlock(); + QCString intro = lne ? lne->intro() : QCString("External requirements specifications."); + ol.parseText(intro); + ol.endTextBlock(); + + ol.startItemList(); + for (const auto &collection : collections) + { + if (collection.requirements.empty()) + { + continue; + } + + ol.startItemListItem(); + QCString displayTitle = collection.displayTitle; + QCString linkUrl; + bool isExternal = false; + + // Create link to requirements documentation + if (!collection.pageFilename.isEmpty()) + { + // Determine if this is an external URL or internal page + if (collection.destination.isEmpty()) + { + // Internal page from REQUIREMENTS_FILES (pageFilename is the Doxygen-generated page) + linkUrl = collection.pageFilename; + isExternal = false; + } + else if (collection.destination.contains("://")) + { + // Full URL with protocol (http://, https://, file://, etc.) + linkUrl = collection.destination + "/" + collection.pageFilename; + isExternal = true; + } + else + { + // Relative or absolute path from REQUIREMENTS_TAGFILES + // destination is the path (e.g., "../../requirements/html") + // pageFilename is from tag file (e.g., "index.html") + linkUrl = collection.destination + "/" + collection.pageFilename; + isExternal = true; + } + + ol.writeString(""); + } + else + { + ol.writeString("\">"); + } + ol.writeString(displayTitle); + ol.writeString(""); + } + else + { + ol.writeString(displayTitle); + } + + if (addToIndex && !linkUrl.isEmpty()) + { + Doxygen::indexList->addContentsItem(FALSE,displayTitle,QCString(),linkUrl,QCString(),FALSE,TRUE); + } + + ol.endItemListItem(); + } + ol.endItemList(); + + if (addToIndex) + { + Doxygen::indexList->decContentsDepth(); + } + + endFile(ol); + ol.popGeneratorState(); +} + +//---------------------------------------------------------------------------- + +void writeTraceabilityIndex(OutputList &ol) +{ + RequirementsTracker &tracker = RequirementsTracker::instance(); + if (!tracker.hasRequirements()) return; + + const auto &collections = tracker.collections(); + if (collections.empty()) return; + + ol.pushGeneratorState(); + ol.disable(OutputType::Man); + ol.disable(OutputType::Docbook); + + LayoutNavEntry *lne = LayoutDocManager::instance().rootNavEntry()->find(LayoutNavEntry::Traceability); + QCString title = lne ? lne->title() : QCString("Traceability"); + bool addToIndex = lne==nullptr || lne->visible(); + + startFile(ol,"traceability",false,QCString(),title,HighlightedItem::Traceability); + + startTitle(ol,QCString()); + ol.parseText(title); + endTitle(ol,QCString(),QCString()); + + ol.startContents(); + + if (addToIndex) + { + Doxygen::indexList->addContentsItem(TRUE,title,QCString(),"traceability",QCString(),TRUE,TRUE); + Doxygen::indexList->incContentsDepth(); + } + + ol.startTextBlock(); + QCString intro = lne ? lne->intro() : QCString("Summary of imported requirements specifications."); + ol.parseText(intro); + ol.endTextBlock(); + + ol.startItemList(); + for (const auto &collection : collections) + { + if (collection.requirements.empty()) + { + continue; + } + + // Count requirements with references + int withSatisfiedBy = 0; + int withVerifiedBy = 0; + for (const auto &[reqId, req] : collection.requirements) { + if (!req.satisfiedBy.empty()) withSatisfiedBy++; + if (!req.verifiedBy.empty()) withVerifiedBy++; + } + + ol.startItemListItem(); + QCString displayTitle = collection.displayTitle; + QCString pageBase = collection.pageName; + + ol.writeObjectLink(QCString(),pageBase,QCString(),displayTitle); + + // Build summary: "— 434 Requirements (100.0% Satisfied, 4.4% Verified)" + size_t totalReqs = collection.requirements.size(); + double satisfiedPct = 100.0 * withSatisfiedBy / (double)totalReqs; + double verifiedPct = 100.0 * withVerifiedBy / (double)totalReqs; + + QCString metaText; + metaText.sprintf(" — %d %s (%.1f%% %s, %.1f%% %s)", + (int)totalReqs, + qPrint(theTranslator->trRequirements()), + satisfiedPct, + qPrint(theTranslator->trSatisfied()), + verifiedPct, + qPrint(theTranslator->trVerified())); + ol.writeString(metaText.data()); + + if (addToIndex) + { + Doxygen::indexList->addContentsItem(FALSE,displayTitle,QCString(),pageBase,QCString(),FALSE,TRUE); + } + + ol.endItemListItem(); + } + ol.endItemList(); + + if (addToIndex) + { + Doxygen::indexList->decContentsDepth(); + } + + endFile(ol); + ol.popGeneratorState(); +} + + //---------------------------------------------------------------------------- static void countRelatedPages(int &docPages,int &indexPages) @@ -4017,6 +4222,32 @@ static void writePageIndex(OutputList &ol) writePages(pd.get(),&ftv); } } + + // Add Requirements section with external links to requirements documentation + RequirementsTracker &tracker = RequirementsTracker::instance(); + if (tracker.hasRequirements()) + { + const auto &collections = tracker.collections(); + if (!collections.empty()) + { + // Add a "Requirements" parent item + ftv.addContentsItem(true, "Requirements", QCString(), QCString(), QCString(), false, true, nullptr); + ftv.incContentsDepth(); + + for (const auto &collection : collections) + { + if (!collection.requirements.empty() && !collection.destination.isEmpty()) + { + // Create external link to requirements documentation + QCString externalUrl = collection.destination + "/index.html"; + ftv.addContentsItem(false, collection.displayTitle, QCString(), externalUrl, QCString(), false, true, nullptr); + } + } + + ftv.decContentsDepth(); + } + } + TextStream t; ftv.generateTreeViewInline(t); ol.writeString(t.str()); @@ -5502,6 +5733,42 @@ static void writeIndexHierarchyEntries(OutputList &ol,const LayoutNavEntryList & msg("Generating example index...\n"); writeExampleIndex(ol); break; + case LayoutNavEntry::Requirements: + { + bool hasReq = RequirementsTracker::instance().hasRequirements(); + lne->setVisible(hasReq); + if (!hasReq) + { + break; + } + if (addToIndex) + { + Doxygen::indexList->addContentsItem(TRUE,lne->title(),QCString(),lne->baseFile(),QCString()); + Doxygen::indexList->incContentsDepth(); + needsClosing=TRUE; + } + msg("Generating requirements index...\n"); + writeRequirementsIndex(ol); + } + break; + case LayoutNavEntry::Traceability: + { + bool hasReq = RequirementsTracker::instance().hasRequirements(); + lne->setVisible(hasReq); + if (!hasReq) + { + break; + } + if (addToIndex) + { + Doxygen::indexList->addContentsItem(TRUE,lne->title(),QCString(),lne->baseFile(),QCString()); + Doxygen::indexList->incContentsDepth(); + needsClosing=TRUE; + } + msg("Generating traceability index...\n"); + writeTraceabilityIndex(ol); + } + break; case LayoutNavEntry::User: if (addToIndex) { @@ -5564,6 +5831,7 @@ static void writeIndexHierarchyEntries(OutputList &ol,const LayoutNavEntryList & case LayoutNavEntry::Namespaces: case LayoutNavEntry::Classes: case LayoutNavEntry::Files: + case LayoutNavEntry::Traceability: case LayoutNavEntry::UserGroup: Doxygen::indexList->decContentsDepth(); break; @@ -5618,6 +5886,8 @@ static bool quickLinkVisible(LayoutNavEntry::Kind kind) case LayoutNavEntry::FileList: return index.numDocumentedFiles()>0 && showFiles; case LayoutNavEntry::FileGlobals: return index.numDocumentedFileMembers(FileMemberHighlight::All)>0; case LayoutNavEntry::Examples: return !Doxygen::exampleLinkedMap->empty(); + case LayoutNavEntry::Requirements: return RequirementsTracker::instance().hasRequirements(); + case LayoutNavEntry::Traceability: return RequirementsTracker::instance().hasRequirements(); case LayoutNavEntry::None: // should never happen, means not properly initialized assert(kind != LayoutNavEntry::None); return FALSE; diff --git a/src/index.h b/src/index.h index 810e3a29ae1..0eca8af0dce 100644 --- a/src/index.h +++ b/src/index.h @@ -81,6 +81,8 @@ enum class HighlightedItem Globals, Pages, Examples, + Requirements, + Traceability, Search, UserGroup, @@ -223,6 +225,8 @@ class Index void writeGraphInfo(OutputList &ol); void writeIndexHierarchy(OutputList &ol); +void writeRequirementsIndex(OutputList &ol); +void writeTraceabilityIndex(OutputList &ol); void startTitle(OutputList &ol,const QCString &fileName,const DefinitionMutable *def=nullptr); void endTitle(OutputList &ol,const QCString &fileName,const QCString &name); void startFile(OutputList &ol,const QCString &name,bool isSource,const QCString &manName, diff --git a/src/latexdocvisitor.cpp b/src/latexdocvisitor.cpp index 7e815517cfd..98fcfd395c5 100644 --- a/src/latexdocvisitor.cpp +++ b/src/latexdocvisitor.cpp @@ -938,6 +938,14 @@ void LatexDocVisitor::operator()(const DocSimpleSect &s) m_t << "\\begin{DoxyImportant}{"; filter(theTranslator->trImportant()); break; + case DocSimpleSect::Satisfies: + m_t << "\\begin{DoxyParagraph}{"; + filter(theTranslator->trSatisfies()); + break; + case DocSimpleSect::Verifies: + m_t << "\\begin{DoxyParagraph}{"; + filter(theTranslator->trVerifies()); + break; case DocSimpleSect::User: m_t << "\\begin{DoxyParagraph}{"; break; diff --git a/src/layout.cpp b/src/layout.cpp index f0034d9e06e..31175a77f15 100644 --- a/src/layout.cpp +++ b/src/layout.cpp @@ -523,6 +523,20 @@ class LayoutParser theTranslator->trExamplesDescription(), "examples" }, + { "requirements", + LayoutNavEntry::Requirements, + theTranslator->trRequirements(), + QCString(), + theTranslator->trRequirementsDescription(), + "requirements" + }, + { "traceability", + LayoutNavEntry::Traceability, + theTranslator->trTraceability(), + QCString(), + theTranslator->trTraceabilityDescription(), + "traceability" + }, { "user", LayoutNavEntry::User, QCString(), diff --git a/src/layout.h b/src/layout.h index a4ea6593ed8..9b0de292fdc 100644 --- a/src/layout.h +++ b/src/layout.h @@ -186,6 +186,8 @@ struct LayoutNavEntry NSPEC(FileList,) \ NSPEC(FileGlobals,) \ NSPEC(Examples,) \ + NSPEC(Requirements,) \ + NSPEC(Traceability,) \ NSPEC(User,) \ NSPEC(UserGroup,) diff --git a/src/mandocvisitor.cpp b/src/mandocvisitor.cpp index e2ea0e12931..7587cec1b01 100644 --- a/src/mandocvisitor.cpp +++ b/src/mandocvisitor.cpp @@ -565,6 +565,10 @@ void ManDocVisitor::operator()(const DocSimpleSect &s) m_t << theTranslator->trAttention(); break; case DocSimpleSect::Important: m_t << theTranslator->trImportant(); break; + case DocSimpleSect::Satisfies: + m_t << theTranslator->trSatisfies(); break; + case DocSimpleSect::Verifies: + m_t << theTranslator->trVerifies(); break; case DocSimpleSect::User: break; case DocSimpleSect::Rcs: break; case DocSimpleSect::Unknown: break; diff --git a/src/perlmodgen.cpp b/src/perlmodgen.cpp index 83858bbd7f0..f6ea843a462 100644 --- a/src/perlmodgen.cpp +++ b/src/perlmodgen.cpp @@ -840,6 +840,8 @@ void PerlModDocVisitor::operator()(const DocSimpleSect &s) case DocSimpleSect::Important: type = "important"; break; case DocSimpleSect::User: type = "par"; break; case DocSimpleSect::Rcs: type = "rcs"; break; + case DocSimpleSect::Satisfies: type = "satisfies"; break; + case DocSimpleSect::Verifies: type = "verifies"; break; case DocSimpleSect::Unknown: err("unknown simple section found\n"); break; diff --git a/src/printdocvisitor.h b/src/printdocvisitor.h index dc8f5c78e30..2fdfd80e6d4 100644 --- a/src/printdocvisitor.h +++ b/src/printdocvisitor.h @@ -370,6 +370,8 @@ class PrintDocVisitor case DocSimpleSect::Important: printf("important"); break; case DocSimpleSect::User: printf("user"); break; case DocSimpleSect::Rcs: printf("rcs"); break; + case DocSimpleSect::Satisfies: printf("satisfies"); break; + case DocSimpleSect::Verifies: printf("verifies"); break; case DocSimpleSect::Unknown: printf("unknown"); break; } printf(">\n"); diff --git a/src/requirementstracker.cpp b/src/requirementstracker.cpp new file mode 100644 index 00000000000..53838610465 --- /dev/null +++ b/src/requirementstracker.cpp @@ -0,0 +1,927 @@ +/**************************************************************************** + * + * Requirements traceability support for Doxygen. + * + ****************************************************************************/ + +#include "requirementstracker.h" + +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "debug.h" +#include "doxygen.h" +#include "entry.h" +#include "filedef.h" +#include "filename.h" +#include "fileinfo.h" +#include "message.h" +#include "pagedef.h" +#include "parserintf.h" +#include "util.h" +#include "xml.h" +#include "outputlist.h" +#include "index.h" +#include "definition.h" +#include "memberdef.h" +#include "doxygen.h" +#include "htmlgen.h" +#include "latexgen.h" +#include "rtfgen.h" +#include "trace.h" +#include "classdef.h" +#include "namespacedef.h" +#include "language.h" + +namespace { +// Context structure for XML parsing +struct XMLContext { + bool insideCompound = false; + bool collectingName = false; + bool collectingTitle = false; + bool collectingFilename = false; + bool collectingAnchor = false; + QCString currentText; + QCString currentFile; + QCString pageFilename; // The element +}; + +QCString stripQuotes(const QCString &input) { + if (input.length() >= 2) { + char first = input.at(0); + char last = input.at(input.length() - 1); + if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) { + return input.mid(1, input.length() - 2).stripWhiteSpace(); + } + } + return input; +} +} // namespace + +RequirementsTracker &RequirementsTracker::instance() { + static RequirementsTracker inst; + return inst; +} + +void RequirementsTracker::initialize() { + if (m_initialized) { + return; + } + + // Don't clear collections if they already exist and have references + // This preserves @satisfies/@verifies collected during document parsing + bool hasExistingData = false; + for (const auto &collection : m_collections) { + for (const auto &[reqId, req] : collection.requirements) { + if (!req.satisfiedBy.empty() || !req.verifiedBy.empty()) { + hasExistingData = true; + break; + } + } + if (hasExistingData) break; + } + + if (!hasExistingData) { + m_collections.clear(); + } + + // First, parse REQUIREMENTS_FILES (direct source files) + const StringVector &files = Config_getList(REQUIREMENTS_FILES); + if (!files.empty()) { + std::unordered_set processedFilePaths; + for (const auto &file : files) { + if (file.empty()) { + continue; + } + + if (!processedFilePaths.insert(file).second) { + continue; // avoid parsing the same file twice + } + + QCString filePath(file.c_str()); + filePath = filePath.stripWhiteSpace(); + + if (filePath.isEmpty()) { + continue; + } + + FileInfo fi(filePath.str()); + if (!fi.exists() || !fi.isFile()) { + warn(filePath, 1, "requirements file '{}' could not be located", filePath); + continue; + } + + // Create a new collection and parse the file + RequirementsCollection collection; + collection.tagFilePath = QCString(fi.absFilePath().c_str()); + + // Pass the original (possibly relative) path for page name generation + parseRequirementsFile(collection, collection.tagFilePath, filePath); + + if (!collection.requirements.empty()) { + msg("Loaded {} requirements from '{}'", collection.requirements.size(), collection.tagFilePath); + m_collections.push_back(std::move(collection)); + } + } + } + + // Then, parse REQUIREMENTS_TAGFILES (external tag files) + const StringVector &entries = Config_getList(REQUIREMENTS_TAGFILES); + if (!entries.empty()) { + std::unordered_set processedPaths; + for (const auto &entry : entries) { + if (entry.empty()) { + continue; + } + + QCString spec(entry.c_str()); + + if (!processedPaths.insert(entry).second) { + continue; // avoid parsing the same specification twice + } + + QCString tagPath; + QCString destination; + int eqPos = spec.find('='); + if (eqPos != -1) { + tagPath = spec.left(eqPos).stripWhiteSpace(); + destination = spec.mid(eqPos + 1).stripWhiteSpace(); + } else { + tagPath = spec.stripWhiteSpace(); + } + + if (tagPath.isEmpty()) { + continue; + } + + FileInfo fi(tagPath.str()); + if (!fi.exists() || !fi.isFile()) { + warn(tagPath, 1, "requirements tag file '{}' could not be located", + tagPath); + continue; + } + + // Create a new collection and parse the tag file + RequirementsCollection collection; + collection.tagFilePath = QCString(fi.absFilePath().c_str()); + collection.destination = stripQuotes(destination); + + parseRequirementsTagFile(collection, collection.tagFilePath, collection.destination); + + if (!collection.requirements.empty()) { + msg("Loaded {} requirements from '{}'", collection.requirements.size(), collection.tagFilePath); + m_collections.push_back(std::move(collection)); + } + } + } + + m_initialized = true; +} + +bool RequirementsTracker::hasRequirements() const { + return !m_collections.empty(); +} + +void RequirementsTracker::reset() { + m_initialized = false; + m_collections.clear(); + m_entryRequirements.clear(); +} + +void RequirementsTracker::addDocumentationSections() { + AUTO_TRACE(); + + // For each entry that has requirements annotations, find its Definition + // and add documentation sections + for (const auto &info : m_entryRequirements) { + if (!info.entry) continue; + + // Try to find the Definition for this Entry + // This is tricky because we only have the Entry, not the Definition + // We can use the qualified name to try to look it up + QCString qualName = getQualifiedNameForEntry(info.entry.get()); + + if (qualName.isEmpty()) continue; + + // Try to find the definition in various places + Definition *def = nullptr; + + // Try as a class + const ClassDef *cd = getClass(qualName); + if (cd) { + def = const_cast(cd); + } else { + // Try as a namespace + const NamespaceDef *nd = Doxygen::namespaceLinkedMap->find(qualName); + if (nd) { + def = const_cast(nd); + } else { + // Try as a member - need to split into scope and member name + int i = qualName.findRev("::"); + if (i != -1) { + QCString scope = qualName.left(i); + QCString member = qualName.mid(i+2); + + // Try to find the scope first + cd = getClass(scope); + if (cd) { + // Look for the member in the class + const MemberDef *md = cd->getMemberByName(member); + if (md) { + def = const_cast(md); + } + } + } + } + } + + if (!def) continue; // Couldn't find the definition + + // Build the documentation sections + QCString docSection; + + if (!info.satisfies.empty()) { + docSection += "\n\n@par " + theTranslator->trSatisfies() + "\n"; + for (const auto &reqId : info.satisfies) { + // Find the URL for this requirement + RequirementsCollection *collection = findCollectionByPrefix(reqId); + if (collection) { + auto it = collection->requirements.find(reqId); + if (it != collection->requirements.end() && !it->second.url.isEmpty()) { + docSection += "- [" + reqId + "](" + it->second.url + ")\n"; + } else { + docSection += "- " + reqId + "\n"; + } + } + } + } + + if (!info.verifies.empty()) { + docSection += "\n\n@par " + theTranslator->trVerifies() + "\n"; + for (const auto &reqId : info.verifies) { + // Find the URL for this requirement + RequirementsCollection *collection = findCollectionByPrefix(reqId); + if (collection) { + auto it = collection->requirements.find(reqId); + if (it != collection->requirements.end() && !it->second.url.isEmpty()) { + docSection += "- [" + reqId + "](" + it->second.url + ")\n"; + } else { + docSection += "- " + reqId + "\n"; + } + } + } + } + + // Add the documentation section to the Definition + if (!docSection.isEmpty()) { + DefinitionMutable *defm = toDefinitionMutable(def); + if (defm) { + defm->setInbodyDocumentation(docSection, info.entry->docFile, info.entry->docLine); + } + } + } +} + +// Extract prefix from requirement ID (e.g., "SRS_123" -> "SRS_") +QCString RequirementsTracker::extractPrefix(const QCString &requirementId) const { + // Match pattern: (PREFIX)(NUMBER) where PREFIX can be zero or more characters + // Examples: SRS_123 -> "SRS_", REQ123 -> "REQ", ABC-456 -> "ABC-", "123" -> "" + // Use non-greedy match (.*?) to get minimal prefix followed by digits at the end (\\d+$) + std::regex pattern("^(.*?)(\\d+)$"); + std::string id_str = requirementId.str(); + std::smatch match; + + if (std::regex_match(id_str, match, pattern)) { + return QCString(match[1].str().c_str()); + } + + return QCString(); +} + +// Find collection by requirement prefix +RequirementsTracker::RequirementsCollection* RequirementsTracker::findCollectionByPrefix(const QCString &requirementId) { + QCString prefix = extractPrefix(requirementId); + + for (auto &collection : m_collections) { + if (collection.prefix == prefix) { + return &collection; + } + } + return nullptr; +} + +void RequirementsTracker::addSatisfiedBy(const QCString &requirementId, const QCString &reference) { + RequirementsCollection *collection = findCollectionByPrefix(requirementId); + if (!collection) { + return; + } + + auto it = collection->requirements.find(requirementId); + if (it != collection->requirements.end()) { + // Check if this reference already exists to avoid duplicates + auto &satisfiedBy = it->second.satisfiedBy; + if (std::find(satisfiedBy.begin(), satisfiedBy.end(), reference) == satisfiedBy.end()) { + satisfiedBy.push_back(reference); + } + } +} + +void RequirementsTracker::addVerifiedBy(const QCString &requirementId, const QCString &reference) { + RequirementsCollection *collection = findCollectionByPrefix(requirementId); + if (!collection) { + return; + } + + auto it = collection->requirements.find(requirementId); + if (it != collection->requirements.end()) { + // Check if this reference already exists to avoid duplicates + auto &verifiedBy = it->second.verifiedBy; + if (std::find(verifiedBy.begin(), verifiedBy.end(), reference) == verifiedBy.end()) { + verifiedBy.push_back(reference); + } + } +} + +// Parse a requirements tag file and populate the collection +void RequirementsTracker::parseRequirementsTagFile(RequirementsCollection &collection, + const QCString &tagFilePath, + const QCString &destination) { + // Read the tag file + QCString raw = fileToString(tagFilePath); + if (raw.isEmpty()) { + warn(tagFilePath, 1, "requirements tag file '{}' is empty", tagFilePath); + return; + } + + XMLHandlers handlers; + XMLContext context; + + // Handler for + handlers.startElement = [&](const std::string &name, const XMLHandlers::Attributes &attrs) { + if (name == "compound") { + auto kind_it = attrs.find("kind"); + if (kind_it != attrs.end() && kind_it->second == "page") { + context.insideCompound = true; + } + } else if (context.insideCompound && name == "name") { + context.collectingName = true; + context.currentText.clear(); + } else if (context.insideCompound && name == "title") { + context.collectingTitle = true; + context.currentText.clear(); + } else if (context.insideCompound && name == "filename") { + context.collectingFilename = true; + context.currentText.clear(); + } else if (context.insideCompound && name == "docanchor") { + context.collectingAnchor = true; + context.currentText.clear(); + + // Get the file attribute if present + auto file_it = attrs.find("file"); + if (file_it != attrs.end()) { + context.currentFile = QCString(file_it->second.c_str()); + } + } + return true; + }; + + handlers.endElement = [&](const std::string &name) { + if (name == "compound") { + context.insideCompound = false; + } else if (name == "name" && context.collectingName) { + context.collectingName = false; + // Store the name but prefer title for display + } else if (name == "title" && context.collectingTitle) { + context.collectingTitle = false; + // Use the title as the display title + collection.displayTitle = context.currentText.stripWhiteSpace(); + } else if (name == "filename" && context.collectingFilename) { + context.collectingFilename = false; + context.pageFilename = context.currentText.stripWhiteSpace(); + } else if (name == "docanchor" && context.collectingAnchor) { + context.collectingAnchor = false; + + // This is a requirement ID (anchor) + QCString reqId = context.currentText.stripWhiteSpace(); + if (!reqId.isEmpty()) { + // If this is the first requirement, establish the collection prefix + if (collection.prefix.isEmpty()) { + QCString prefix = extractPrefix(reqId); + + // Skip if it doesn't match the requirement pattern (e.g., "mainpage") + if (prefix.isEmpty()) { + context.currentFile.clear(); + return true; + } + + // Store the prefix for this collection + collection.prefix = prefix; + } + + // Check if this requirement matches the collection's requirement pattern + QCString prefix = extractPrefix(reqId); + + // Only add requirements that match the pattern AND have the same prefix + if (!prefix.isEmpty() && prefix == collection.prefix) { + Requirement req; + req.id = reqId; + + // Build URL using the file from the docanchor attribute or the page filename + QCString fileToUse = context.currentFile.isEmpty() ? context.pageFilename : context.currentFile; + if (!destination.isEmpty() && !fileToUse.isEmpty()) { + req.url = destination + "/" + fileToUse + "#" + reqId; + } + + collection.requirements[reqId] = req; + } + } + + context.currentFile.clear(); + } + return true; + }; + + handlers.characters = [&](const std::string &chars) { + if (context.collectingName || context.collectingTitle || context.collectingFilename || context.collectingAnchor) { + context.currentText += QCString(chars.c_str()); + } + }; + + handlers.error = [&](const std::string &fileName, int lineNr, const std::string &msg) { + warn(fileName.c_str(), lineNr, "{}", msg); + }; + + XMLParser parser(handlers); + parser.parse(tagFilePath.data(), raw.data(), Debug::isFlagSet(Debug::Lex_xml), + []() {}, []() {}); + + // Generate page name from display title or file name + if (collection.displayTitle.isEmpty()) { + FileInfo fi(tagFilePath.str()); + collection.displayTitle = QCString(fi.fileName().c_str()); + } + + // Store the page filename from the tag file + collection.pageFilename = context.pageFilename; + + // Create page name like "traceability_srs_123456" + QCString pageName = "traceability_" + collection.displayTitle.lower(); + // Sanitize: replace spaces and problematic characters with underscores + pageName = substitute(pageName, ' ', '_'); + pageName = substitute(pageName, '-', '_'); + collection.pageName = pageName; +} + +// Parse a requirements file directly (markdown, doxygen, source files) +// Parse a requirements file and populate the collection +void RequirementsTracker::parseRequirementsFile(RequirementsCollection &collection, + const QCString &absFilePath, + const QCString &originalPath) { + // Read the file + QCString raw = fileToString(absFilePath); + if (raw.isEmpty()) { + warn(absFilePath, 1, "requirements file '{}' is empty", absFilePath); + return; + } + + // Generate the page name that Doxygen will create for this markdown file + // This must match the logic in markdown.cpp's markdownFileNameToId() + // which does: absFilePath -> stripFromPath -> remove extension -> escapeCharsInString + + // Use absolute path, then strip from path (matches STRIP_FROM_PATH config) + std::string absFilePathStr = FileInfo(originalPath.str()).absFilePath(); + QCString baseFn = stripFromPath(absFilePathStr.c_str()); + + // Remove extension + int i = baseFn.findRev('.'); + if (i != -1) { + baseFn = baseFn.left(i); + } + + // Apply the same escaping that Doxygen uses (allowDots=false, allowUnderscore=false) + QCString baseName = escapeCharsInString(baseFn, false, false); + + // Generate page filename based on file extension + // Only markdown files (.md, .markdown) get the "md_" prefix + // Other Doxygen document formats (.dox, .txt, .doc) use the base name directly + QCString pageFileName; + QCString extension = originalPath.lower(); + if (extension.endsWith(".md") || extension.endsWith(".markdown")) { + pageFileName = "md_" + baseName; + } else { + // For .dox, .txt, .doc and other formats, no prefix is added + pageFileName = baseName; + } + + // Pattern to match @anchor or \anchor followed by requirement ID + // Examples: @anchor SRS_123, \anchor REQ_456 + // Doxygen supports both @ (Javadoc style) and \ (Qt style) command prefixes + std::regex anchorPattern(R"([@\\]anchor\s+([A-Za-z0-9_-]+))"); + + // Extract the page title from markdown content + // Look for either ATX-style (# Title) or Setext-style (Title\n===) + std::regex atxTitlePattern(R"(^\s*#\s+(.+)$)", std::regex::multiline); + std::regex setextTitlePattern(R"(^(.+)\n=+\s*$)", std::regex::multiline); + + QCString pageTitle; + std::string content = raw.str(); + + // Try Setext-style first (Title\n===) as it's used in the requirements file + std::smatch titleMatch; + if (std::regex_search(content, titleMatch, setextTitlePattern)) { + pageTitle = QCString(titleMatch[1].str().c_str()).stripWhiteSpace(); + } + // Fallback to ATX-style (# Title) + else if (std::regex_search(content, titleMatch, atxTitlePattern)) { + pageTitle = QCString(titleMatch[1].str().c_str()).stripWhiteSpace(); + } + + // Strip Doxygen {#labelid} Header Id Attributes from the title + if (!pageTitle.isEmpty()) { + std::regex idTagPattern(R"(\s*\{#[^}]+\})"); + std::string titleStr = pageTitle.str(); + titleStr = std::regex_replace(titleStr, idTagPattern, ""); + pageTitle = QCString(titleStr.c_str()).stripWhiteSpace(); + } + + std::sregex_iterator it(content.begin(), content.end(), anchorPattern); + std::sregex_iterator end; + + for (; it != end; ++it) { + std::smatch match = *it; + QCString reqId = QCString(match[1].str().c_str()); + + // If this is the first requirement, establish the collection prefix + if (collection.prefix.isEmpty()) { + QCString prefix = extractPrefix(reqId); + + // Skip if it doesn't match a requirement pattern (must have digits at end) + if (prefix.isEmpty()) { + continue; + } + + // Store the prefix for this collection + collection.prefix = prefix; + + // Use the extracted page title if available, otherwise generate from prefix + if (!pageTitle.isEmpty()) { + collection.displayTitle = pageTitle; + } else { + // Generate display title from the prefix (remove trailing underscore/dash) + collection.displayTitle = prefix; + if (collection.displayTitle.endsWith("_") || collection.displayTitle.endsWith("-")) { + collection.displayTitle = collection.displayTitle.left(collection.displayTitle.length() - 1); + } + } + + // Create page name for traceability - must match generateTraceabilityPage() logic + QCString pageName = "traceability_" + collection.displayTitle.lower(); + pageName = substitute(pageName, ' ', '_'); + pageName = substitute(pageName, '-', '_'); + collection.pageName = pageName; + + // Store the page filename (the generated .html file) + collection.pageFilename = pageFileName + ".html"; + + // For REQUIREMENTS_FILES, destination is empty (internal page) + collection.destination = ""; + } + + // Check if this requirement matches the collection's requirement pattern + QCString prefix = extractPrefix(reqId); + + // Only add requirements that match the pattern AND have the same prefix + if (!prefix.isEmpty() && prefix == collection.prefix) { + Requirement req; + req.id = reqId; + + // URL points to the generated markdown page with the anchor + req.url = pageFileName + ".html#" + reqId; + + collection.requirements[reqId] = req; + } + } + + // If no requirements found, warn the user + if (collection.requirements.empty()) { + warn(absFilePath, 1, "no requirement anchors found in file '{}' (expected @anchor PATTERN)", absFilePath); + } +} + +void RequirementsTracker::generateTraceabilityPages() { + initialize(); + + if (m_collections.empty()) { + return; + } + + msg("Generating {} traceability pages...", m_collections.size()); + + for (const auto &collection : m_collections) { + msg(" Collection '{}': {} requirements", collection.displayTitle, collection.requirements.size()); + + // Count requirements with references + int withReferences = 0; + for (const auto &[reqId, req] : collection.requirements) { + if (!req.satisfiedBy.empty() || !req.verifiedBy.empty()) { + withReferences++; + } + } + msg(" {} requirements have satisfied/verified references", withReferences); + + // Generate the page + generateTraceabilityPage(collection); + } +} + +void RequirementsTracker::generateTraceabilityPage(const RequirementsCollection &collection) { + // Generate page name based on collection + QCString pageName = "traceability_" + collection.displayTitle.lower(); + pageName = substitute(pageName, " ", "_"); + pageName = substitute(pageName, "-", "_"); + + // Check if page already exists + if (Doxygen::pageLinkedMap->find(pageName)) { + msg("Traceability page '{}' already exists, skipping.\n", pageName); + return; + } + + // Count requirements with references + int withSatisfiedBy = 0; + int withVerifiedBy = 0; + for (const auto &[reqId, req] : collection.requirements) { + if (!req.satisfiedBy.empty()) withSatisfiedBy++; + if (!req.verifiedBy.empty()) withVerifiedBy++; + } + + // Build documentation string in memory using HTML table syntax + // Doxygen will convert this properly to all output formats (HTML, LaTeX, RTF, etc.) + QCString docContent; + + // Write summary statistics using translators for internationalization + size_t totalReqs = collection.requirements.size(); + docContent += QCString().setNum((int)totalReqs) + " " + theTranslator->trRequirements() + "\n\n"; + + double satisfiedPct = 100.0 * withSatisfiedBy / (double)totalReqs; + char satisfiedBuf[32]; + snprintf(satisfiedBuf, sizeof(satisfiedBuf), "%.1f", satisfiedPct); + docContent += QCString().setNum(withSatisfiedBy) + " " + theTranslator->trSatisfied() + + " (" + satisfiedBuf + "%)\n\n"; + + double verifiedPct = 100.0 * withVerifiedBy / (double)totalReqs; + char verifiedBuf[32]; + snprintf(verifiedBuf, sizeof(verifiedBuf), "%.1f", verifiedPct); + docContent += QCString().setNum(withVerifiedBy) + " " + theTranslator->trVerified() + + " (" + verifiedBuf + "%)\n\n"; + + // Create table using HTML syntax (Doxygen converts this to all output formats) + docContent += "\n"; + docContent += "\n"; + docContent += "\n"; + + // Write each requirement row + for (const auto &[reqId, req] : collection.requirements) { + docContent += "\n"; + } + + docContent += "
" + theTranslator->trTraceabilityFor(collection.displayTitle) + "
" + theTranslator->trRequirementID() + "" + + theTranslator->trSatisfiedBy() + "" + + theTranslator->trVerifiedBy() + "
"; + + // Requirement ID with link to external documentation + if (!req.url.isEmpty()) { + docContent += "" + reqId + ""; + } else { + docContent += reqId; + } + docContent += ""; + + // Satisfied By column with \ref links + if (!req.satisfiedBy.empty()) { + for (size_t i = 0; i < req.satisfiedBy.size(); ++i) { + if (i > 0) docContent += "\n\n"; // Double newline creates separate paragraphs + docContent += "\\ref " + req.satisfiedBy[i]; + } + } + docContent += ""; + + // Verified By column with \ref links + if (!req.verifiedBy.empty()) { + for (size_t i = 0; i < req.verifiedBy.size(); ++i) { + if (i > 0) docContent += "\n\n"; // Double newline creates separate paragraphs + docContent += "\\ref " + req.verifiedBy[i]; + } + } + docContent += "
\n"; + + // Create page title using translator + QCString pageTitle = theTranslator->trTraceabilityFor(collection.displayTitle); + + // Create PageDef directly in memory + // Parameters: fileName, lineNumber, pageName, documentation, title + auto pd = createPageDef( + "", // fileName - use placeholder since it's generated + 1, // lineNumber + pageName, // page name (used as ID) + docContent, // documentation content + pageTitle // page title + ); + + // Set additional properties + pd->setLanguage(SrcLangExt::Cpp); + + // Add the page to Doxygen's page collection + Doxygen::pageLinkedMap->add(pageName, std::move(pd)); + + msg("Generated traceability page: {}\n", pageName); +} + +// Helper function to build a qualified name from an Entry by walking up the tree +QCString RequirementsTracker::getQualifiedNameForEntry(const Entry *entry) const { + if (!entry) return QCString(); + + QCString result; + + // If entry has an 'inside' field, it provides the containing scope (namespace or class) + // The 'name' field then contains the member name, possibly with immediate parent class + if (!entry->inside.isEmpty()) { + // inside gives us the enclosing scope (e.g., "outer::inner::") + // name might be "ClassName::memberName" or just "memberName" + // However, for CLASS entries, name can be fully qualified (e.g., "outer::inner::ClassName") + // which duplicates the inside prefix. Strip it if present. + result = entry->inside; + QCString nameToAdd = entry->name; + + // Strip the inside prefix from name if name starts with it + QCString insidePrefix = entry->inside; + if (insidePrefix.endsWith("::")) { + insidePrefix = insidePrefix.left(insidePrefix.length() - 2); + } + if (nameToAdd.startsWith(insidePrefix + "::")) { + nameToAdd = nameToAdd.mid(insidePrefix.length() + 2); + } + + if (!result.endsWith("::")) { + result += "::"; + } + result += nameToAdd; + return result; + } + + // No 'inside' field - build qualified name by walking up to find the enclosing scope + // Note: Doxygen may store nested namespace declarations with qualified names + // (e.g., "namespace outer::inner" creates an Entry with name="outer::inner") + // In such cases, we should use that name directly and stop, as it already contains + // the full scope path. This works across languages (C++ uses ::, Java/C# use ., etc.) + + QCString scopeName; + const Entry *current = entry->parent(); + + // Look for the first parent that is a scope (class or namespace) + while (current) { + if (!current->name.isEmpty() && + (current->section.isClass() || + current->section.isNamespace())) { + // Found the enclosing scope - use its name + // If it contains a scope separator (:: or .), it's already a full path + scopeName = current->name; + break; + } + current = current->parent(); + } + + // Build the result from the scope name + if (!scopeName.isEmpty()) { + result = scopeName; + // Don't add separator here - it will be added later when appending entry->name + } + + // Finally add the entry's own name if it has one and we built a scope + if (!entry->name.isEmpty() && + (entry->section.isFunction() || + entry->section.isVariable() || + entry->section.isTypedef() || + entry->section.isEnum())) { + if (!result.isEmpty() && !result.endsWith("::") && !result.endsWith(".")) { + result += "::"; // Default to :: for C++ + } + result += entry->name; + } else if (!entry->name.isEmpty() && result.isEmpty()) { + // If no scope was built but entry has a name, use it + result = entry->name; + } + + + return result; +} + +void RequirementsTracker::collectFromEntries(const std::shared_ptr &root) { + AUTO_TRACE(); + + if (!root) return; + + // Check if this entry has requirements annotations + bool hasSatisfies = !root->satisfies.empty(); + bool hasVerifies = !root->verifies.empty(); + + // Only process entries that represent documentable entities + // (not intermediate nodes or statements) + bool isDocumentable = !root->name.isEmpty() && + (root->section.isClass() || + root->section.isNamespace() || + root->section.isFunction() || + root->section.isVariable() || + root->section.isTypedef() || + root->section.isEnum() || + root->section.isConcept()); + + if ((hasSatisfies || hasVerifies) && isDocumentable) { + // Add documentation sections to the Entry so they appear in the generated docs + QCString docSection; + + if (hasSatisfies) { + docSection += "\n\n@par " + theTranslator->trSatisfies() + "\n"; + for (const auto &reqId : root->satisfies) { + // Use Doxygen's \ref command to create cross-references to requirement anchors + // This works across all output formats (HTML, LaTeX, RTF, etc.) + docSection += "- \\ref " + reqId + "\n"; + } + } + + if (hasVerifies) { + docSection += "\n\n@par " + theTranslator->trVerifies() + "\n"; + for (const auto &reqId : root->verifies) { + // Use Doxygen's \ref command to create cross-references to requirement anchors + docSection += "- \\ref " + reqId + "\n"; + } + } + + // Add the documentation section to the Entry + if (!docSection.isEmpty()) { + root->doc += docSection; + } + } + + // Process satisfies and verifies from this entry for the traceability table + for (const auto &reqId : root->satisfies) { + if (!reqId.isEmpty()) { + // Find which collection this requirement belongs to + RequirementsCollection* collection = findCollectionByPrefix(reqId); + if (collection) { + // Create a reference string for this code element using qualified name + QCString reference = getQualifiedNameForEntry(root.get()); + + // Fallback if we couldn't build a qualified name + if (reference.isEmpty()) { + if (!root->name.isEmpty()) { + reference = root->name; + } else if (!root->brief.isEmpty()) { + reference = root->brief.left(50); // Truncate long briefs + } else { + reference = root->fileName + ":" + QCString().setNum(root->startLine); + } + } + + addSatisfiedBy(reqId, reference); + } + } + } + + for (const auto &reqId : root->verifies) { + if (!reqId.isEmpty()) { + RequirementsCollection* collection = findCollectionByPrefix(reqId); + if (collection) { + // Create a reference string for this code element using qualified name + QCString reference = getQualifiedNameForEntry(root.get()); + + // Fallback if we couldn't build a qualified name + if (reference.isEmpty()) { + if (!root->name.isEmpty()) { + reference = root->name; + } else if (!root->brief.isEmpty()) { + reference = root->brief.left(50); + } else { + reference = root->fileName + ":" + QCString().setNum(root->startLine); + } + } + + addVerifiedBy(reqId, reference); + } + } + } + + // Recursively process all child entries + for (const auto &child : root->children()) { + collectFromEntries(child); + } +} + +void RequirementsTracker::finalize() { + // Nothing to clean up - pages are now created directly in memory + // and managed by Doxygen's page collection +} diff --git a/src/requirementstracker.h b/src/requirementstracker.h new file mode 100644 index 00000000000..b99d0916be2 --- /dev/null +++ b/src/requirementstracker.h @@ -0,0 +1,91 @@ +/**************************************************************************** + * + * Requirements traceability support for Doxygen. + * + * Copyright (C) 2025 + * + * See the top-level LICENSE file for more details. + * + ****************************************************************************/ + +#ifndef REQUIREMENTSTRACKER_H +#define REQUIREMENTSTRACKER_H + +#include "qcstring.h" + +#include +#include +#include +#include + +class Definition; +class Entry; +class MemberDef; + +class RequirementsTracker { +public: + // Individual requirement with its traceability references + struct Requirement { + QCString id; //!< Requirement ID (anchor from tagfile) + QCString title; //!< Human readable title/summary + QCString url; //!< Full URL to requirement in external docs + std::vector satisfiedBy; //!< Collection of strings referencing what satisfies this + std::vector verifiedBy; //!< Collection of strings referencing what verifies this + }; + + // Collection of requirements with the same prefix (e.g., "A024_SRS_") + struct RequirementsCollection { + QCString prefix; //!< Prefix pattern (e.g., "A024_SRS_") + QCString tagFilePath; //!< Path to the tag file + QCString destination; //!< External documentation base URL or internal page name + QCString pageFilename; //!< Filename from tag file (e.g., "index.html") or generated page name + QCString displayTitle; //!< Friendly title for this collection + QCString pageName; //!< Page name for traceability output + std::map requirements; //!< Map of requirement ID -> Requirement (for quick lookup) + }; + + // Stores information about an Entry that has satisfies/verifies annotations + struct EntryRequirementInfo { + std::shared_ptr entry; //!< The entry with requirements + std::vector satisfies; //!< Requirements it satisfies + std::vector verifies; //!< Requirements it verifies + }; + + static RequirementsTracker &instance(); + + void initialize(); + void collectFromEntries(const std::shared_ptr &root); + void addDocumentationSections(); // Add sections to Definition objects + void generateTraceabilityPages(); + void finalize(); + void reset(); + + bool hasRequirements() const; + + // Get all requirements collections + const std::vector& collections() const { return m_collections; } + + // Get a requirements collection by prefix (e.g., "A024_SRS" -> finds "A024_SRS_" collection) + RequirementsCollection* findCollectionByPrefix(const QCString &requirementId); + + // Add a satisfied/verified reference string to a requirement + void addSatisfiedBy(const QCString &requirementId, const QCString &reference); + void addVerifiedBy(const QCString &requirementId, const QCString &reference); + +private: + RequirementsTracker() = default; + RequirementsTracker(const RequirementsTracker &) = delete; + RequirementsTracker &operator=(const RequirementsTracker &) = delete; + + void parseRequirementsTagFile(RequirementsCollection &collection, const QCString &tagFilePath, const QCString &destination); + void parseRequirementsFile(RequirementsCollection &collection, const QCString &absFilePath, const QCString &originalPath); + QCString extractPrefix(const QCString &requirementId) const; + void generateTraceabilityPage(const RequirementsCollection &collection); + QCString getQualifiedNameForEntry(const Entry *entry) const; + + bool m_initialized = false; + std::vector m_collections; //!< All requirement collections + std::vector m_entryRequirements; //!< Entries with requirement annotations +}; + +#endif diff --git a/src/rtfdocvisitor.cpp b/src/rtfdocvisitor.cpp index 196b4b378b7..f587f0baff8 100644 --- a/src/rtfdocvisitor.cpp +++ b/src/rtfdocvisitor.cpp @@ -805,6 +805,10 @@ void RTFDocVisitor::operator()(const DocSimpleSect &s) m_t << theTranslator->trAttention(); break; case DocSimpleSect::Important: m_t << theTranslator->trImportant(); break; + case DocSimpleSect::Satisfies: + m_t << theTranslator->trSatisfies(); break; + case DocSimpleSect::Verifies: + m_t << theTranslator->trVerifies(); break; case DocSimpleSect::User: break; case DocSimpleSect::Rcs: break; case DocSimpleSect::Unknown: break; diff --git a/src/translator.h b/src/translator.h index 2d3c40ca1a7..5bf5bf4b678 100644 --- a/src/translator.h +++ b/src/translator.h @@ -765,6 +765,38 @@ class Translator ////////////////////////////////////////////////////////////////////////// virtual QCString trImportant() = 0; +////////////////////////////////////////////////////////////////////////// +// new since 1.15.0 +////////////////////////////////////////////////////////////////////////// + /*! Used for the requirements traceability feature */ + virtual QCString trRequirements() = 0; + /*! Used for the traceability index page title */ + virtual QCString trTraceability() = 0; + /** Returns the title for a traceability page for a specific requirement + * @param name The name/title of the requirement + */ + virtual QCString trTraceabilityFor(const QCString &name) = 0; + /*! Used in traceability table header */ + virtual QCString trRequirementID() = 0; + /*! Used in requirements index page */ + virtual QCString trExternalRequirementSpecifications() = 0; + /*! Used in traceability table header */ + virtual QCString trSatisfiedBy() = 0; + /*! Used in traceability table header */ + virtual QCString trVerifiedBy() = 0; + /*! Used in @satisfies/@verifies section documentation */ + virtual QCString trSatisfies() = 0; + /*! Used in @satisfies/@verifies section documentation */ + virtual QCString trVerifies() = 0; + /** Returns the description text for the Requirements index page */ + virtual QCString trRequirementsDescription() = 0; + /** Returns the description text for the Traceability index page */ + virtual QCString trTraceabilityDescription() = 0; + /** Returns "Satisfied" for statistics */ + virtual QCString trSatisfied() = 0; + /** Returns "Verified" for statistics */ + virtual QCString trVerified() = 0; + protected: QCString p_latexCommandName(const QCString &latexCmd) { diff --git a/src/translator_adapter.h b/src/translator_adapter.h index 121a86fe685..ea80b0c2ff6 100644 --- a/src/translator_adapter.h +++ b/src/translator_adapter.h @@ -38,7 +38,40 @@ class TranslatorAdapterBase : public Translator virtual QCString updateNeededMessage() override = 0; }; -class TranslatorAdapter_1_11_0 : public TranslatorAdapterBase +class TranslatorAdapter_1_16_0 : public TranslatorAdapterBase +{ + public: + QCString updateNeededMessage() override + { return createUpdateNeededMessage(idLanguage(),"release 1.16.0"); } + QCString trRequirements() override + { return english.trRequirements(); } + QCString trTraceability() override + { return english.trTraceability(); } + QCString trTraceabilityFor(const QCString &name) override + { return english.trTraceabilityFor(name); } + QCString trRequirementID() override + { return english.trRequirementID(); } + QCString trExternalRequirementSpecifications() override + { return english.trExternalRequirementSpecifications(); } + QCString trSatisfiedBy() override + { return english.trSatisfiedBy(); } + QCString trVerifiedBy() override + { return english.trVerifiedBy(); } + QCString trSatisfies() override + { return english.trSatisfies(); } + QCString trVerifies() override + { return english.trVerifies(); } + QCString trRequirementsDescription() override + { return english.trRequirementsDescription(); } + QCString trTraceabilityDescription() override + { return english.trTraceabilityDescription(); } + QCString trSatisfied() override + { return english.trSatisfied(); } + QCString trVerified() override + { return english.trVerified(); } +}; + +class TranslatorAdapter_1_11_0 : public TranslatorAdapter_1_16_0 { public: QCString updateNeededMessage() override diff --git a/src/translator_br.h b/src/translator_br.h index 4e46c07e515..2df799a19fd 100644 --- a/src/translator_br.h +++ b/src/translator_br.h @@ -168,7 +168,7 @@ namespace PortugueseTranslatorUtils } } -class TranslatorBrazilian : public Translator +class TranslatorBrazilian : public TranslatorAdapter_1_16_0 { public: diff --git a/src/translator_cn.h b/src/translator_cn.h index 9abc0a5adf2..23cca0ec33b 100644 --- a/src/translator_cn.h +++ b/src/translator_cn.h @@ -24,7 +24,7 @@ */ #define CN_SPC " " -class TranslatorChinese : public Translator +class TranslatorChinese : public TranslatorAdapter_1_16_0 { public: /*! Used for identification of the language. The identification diff --git a/src/translator_de.h b/src/translator_de.h index 6b1d0e93cb9..ca1c7c19293 100644 --- a/src/translator_de.h +++ b/src/translator_de.h @@ -146,7 +146,7 @@ #ifndef TRANSLATOR_DE_H #define TRANSLATOR_DE_H -class TranslatorGerman : public Translator +class TranslatorGerman : public TranslatorAdapter_1_16_0 { public: diff --git a/src/translator_en.h b/src/translator_en.h index 607c3ee82a0..0788f62eb5f 100644 --- a/src/translator_en.h +++ b/src/translator_en.h @@ -2612,6 +2612,88 @@ class TranslatorEnglish : public Translator { return "Important"; } + +////////////////////////////////////////////////////////////////////////// +// new since 1.16.0 +////////////////////////////////////////////////////////////////////////// + + /*! Used for the requirements traceability feature */ + QCString trRequirements() override + { + return "Requirements"; + } + + /*! Used for the traceability index page title */ + QCString trTraceability() override + { + return "Traceability"; + } + + /*! Used for traceability page title prefix */ + QCString trTraceabilityFor(const QCString &name) override + { + return "Traceability: " + name; + } + + /*! Used in traceability table header */ + QCString trRequirementID() override + { + return "Requirement ID"; + } + + /*! Used in requirements index page */ + QCString trExternalRequirementSpecifications() override + { + return "External requirements specifications."; + } + + /*! Used in traceability table header */ + QCString trSatisfiedBy() override + { + return "Satisfied By"; + } + + /*! Used in traceability table header */ + QCString trVerifiedBy() override + { + return "Verified By"; + } + + /*! Used in @satisfies/@verifies section documentation */ + QCString trSatisfies() override + { + return "Satisfies"; + } + + /*! Used in @satisfies/@verifies section documentation */ + QCString trVerifies() override + { + return "Verifies"; + } + + /*! Used in requirements index */ + QCString trRequirementsDescription() override + { + return "Here is a list of all requirements with brief descriptions:"; + } + + /*! Used in traceability index */ + QCString trTraceabilityDescription() override + { + return "Here is traceability information for all requirements:"; + } + + /*! Used in traceability statistics */ + QCString trSatisfied() override + { + return "Satisfied"; + } + + /*! Used in traceability statistics */ + QCString trVerified() override + { + return "Verified"; + } }; #endif diff --git a/src/translator_gr.h b/src/translator_gr.h index 5655daf0e61..063ac47d3a9 100644 --- a/src/translator_gr.h +++ b/src/translator_gr.h @@ -16,6 +16,8 @@ */ /* + * 31 Oct 2025 : Updated to "new since 1.16.0" by + * Rob Mellor * 15 Dec 2001 : Translation to greek by * Harry Kalogirou * @@ -50,7 +52,7 @@ #ifndef TRANSLATOR_GR_H #define TRANSLATOR_GR_H -class TranslatorGreek : public Translator +class TranslatorGreek : public TranslatorAdapter_1_16_0 { public: diff --git a/src/translator_lv.h b/src/translator_lv.h index b85f8863f24..f97e890df88 100644 --- a/src/translator_lv.h +++ b/src/translator_lv.h @@ -48,7 +48,7 @@ * Last Doxygen version covered : 1.8.2 */ -class TranslatorLatvian : public Translator +class TranslatorLatvian : public TranslatorAdapter_1_16_0 { public: diff --git a/src/translator_nl.h b/src/translator_nl.h index b5124c9c9ab..577177f831f 100644 --- a/src/translator_nl.h +++ b/src/translator_nl.h @@ -18,7 +18,7 @@ #ifndef TRANSLATOR_NL_H #define TRANSLATOR_NL_H -class TranslatorDutch : public Translator +class TranslatorDutch : public TranslatorAdapter_1_16_0 { public: QCString idLanguage() override diff --git a/src/translator_pl.h b/src/translator_pl.h index a66a861a364..ecf395504be 100644 --- a/src/translator_pl.h +++ b/src/translator_pl.h @@ -24,7 +24,7 @@ #ifndef TRANSLATOR_PL_H #define TRANSLATOR_PL_H -class TranslatorPolish : public Translator +class TranslatorPolish : public TranslatorAdapter_1_16_0 { public: diff --git a/src/translator_pt.h b/src/translator_pt.h index c80e6189978..fa48e0e9b41 100644 --- a/src/translator_pt.h +++ b/src/translator_pt.h @@ -26,6 +26,8 @@ * VERSION HISTORY * --------------- * History: + * 20251031: + * - Updated to 1.16.0; * 20240204: * - Updated to 1.11.0: * 20231107: @@ -81,7 +83,7 @@ #include "translator_br.h" -class TranslatorPortuguese : public Translator +class TranslatorPortuguese : public TranslatorAdapter_1_16_0 { public: diff --git a/src/translator_ru.h b/src/translator_ru.h index 03c99acd0de..f07e8ca5e90 100644 --- a/src/translator_ru.h +++ b/src/translator_ru.h @@ -26,7 +26,7 @@ #ifndef TRANSLATOR_RU_H #define TRANSLATOR_RU_H -class TranslatorRussian : public Translator +class TranslatorRussian : public TranslatorAdapter_1_16_0 { public: /*! Used for identification of the language. */ diff --git a/src/xmldocvisitor.cpp b/src/xmldocvisitor.cpp index e185685f1ef..d872095b423 100644 --- a/src/xmldocvisitor.cpp +++ b/src/xmldocvisitor.cpp @@ -69,6 +69,10 @@ static void startSimpleSect(TextStream &t,const DocSimpleSect &s) t << "attention"; break; case DocSimpleSect::Important: t << "important"; break; + case DocSimpleSect::Satisfies: + t << "satisfies"; break; + case DocSimpleSect::Verifies: + t << "verifies"; break; case DocSimpleSect::User: t << "par"; break; case DocSimpleSect::Rcs: diff --git a/templates/general/layout_default.xml b/templates/general/layout_default.xml index 20d91427d71..350b72c1fa6 100644 --- a/templates/general/layout_default.xml +++ b/templates/general/layout_default.xml @@ -41,6 +41,8 @@ + +