//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "console/console.h" #include "console/engineAPI.h" #include "core/stream/fileStream.h" #include "console/consoleInternal.h" #include "console/compiler.h" #define USE_UNDOCUMENTED_GROUP /// @file /// Documentation generator for the current TorqueScript-based engine API. /// /// Be aware that this generator is solely for the legacy console system and /// is and will not be useful to the new engine API system. It will go away /// when the console interop is removed. /// Used to track unique groups encountered during /// the dump process. static HashTable smDocGroups; static void dumpDoc( Stream& stream, const char* text, bool checkUngrouped = true ) { // Extract brief. String brief; if( text ) { const char* briefTag = dStrstr( text, "@brief" ); if( !briefTag ) { const char* newline = dStrchr( text, '\n' ); if( newline ) { brief = String( text, newline - text ); text = newline + 1; } else { brief = text; text = NULL; } } } // Write doc comment. if( !brief.isEmpty() ) { stream.writeText( "@brief " ); stream.writeText( brief ); stream.writeText( "\r\n\r\n" ); } if( text ) stream.writeText( text ); #ifdef USE_UNDOCUMENTED_GROUP if( checkUngrouped && ( !text || !dStrstr( text, "@ingroup" ) ) ) { smDocGroups.insertUnique( "UNDOCUMENTED", 0 ); stream.writeText( "\r\n@ingroup UNDOCUMENTED\r\n" ); } #endif } static void dumpFragment( Stream& stream, ConsoleDocFragment* fragment ) { if( !fragment->mText || !fragment->mText[ 0 ] ) return; // Emit doc text in comment. stream.writeText( "/*!\r\n" ); stream.writeText( fragment->mText ); stream.writeText( "*/\r\n\r\n" ); // Emit definition, if any. if( fragment->mDefinition ) { stream.writeText( fragment->mDefinition ); stream.writeText( "\r\n" ); } } static void dumpVariable( Stream& stream, Dictionary::Entry* entry, const char* inClass = NULL ) { // Skip variables defined in script. if( entry->type <= Dictionary::Entry::TypeInternalString ) return; // Skip internals... don't export them. if ( entry->mUsage && ( dStrstr( entry->mUsage, "@hide" ) || dStrstr( entry->mUsage, "@internal" ) ) ) return; // Split up qualified name. Vector< String > nameComponents; String( entry->name ).split( "::", nameComponents ); if( !nameComponents.size() ) // Safety check. return; // Match filter. if( inClass ) { // Make sure first qualifier in name components is a // namespace qualifier matching the given class name. if( nameComponents.size() <= 1 || dStricmp( nameComponents.first().c_str() + 1, inClass ) != 0 ) // Skip '$'. return; } else { // Make sure, this is *not* in a class namespace. if( nameComponents.size() > 1 && Con::lookupNamespace( nameComponents.first().c_str() + 1 )->mClassRep ) return; } // Skip variables for which we can't decipher their type. ConsoleBaseType* type = ConsoleBaseType::getType( entry->type ); if( !type ) { Con::errorf( "Can't find type for variable '%s'", entry->name ); return; } // Write doc comment. stream.writeText( "/*!\r\n" ); if( !inClass ) { stream.writeText( "@var " ); stream.writeText( type->getTypeClassName() ); stream.writeText( " " ); stream.writeText( entry->name ); stream.writeText( ";\r\n" ); } dumpDoc( stream, entry->mUsage ); stream.writeText( "*/\r\n" ); // Write definition. const U32 numNameComponents = nameComponents.size(); if( !inClass && numNameComponents > 1 ) for( U32 i = 0; i < ( numNameComponents - 1 ); ++ i ) { stream.writeText( "namespace " ); stream.writeText( nameComponents[ i ] ); stream.writeText( " { " ); } if( inClass ) stream.writeText( "static " ); if( entry->mIsConstant ) stream.writeText( "const " ); stream.writeText( type->getTypeClassName() ); stream.writeText( " " ); stream.writeText( nameComponents.last() ); stream.writeText( ";" ); if( !inClass && numNameComponents > 1 ) for( U32 i = 0; i < ( numNameComponents - 1 ); ++ i ) stream.writeText( " } " ); stream.writeText( "\r\n" ); } static void dumpVariables( Stream& stream, const char* inClass = NULL ) { const U32 hashTableSize = gEvalState.globalVars.hashTable->size; for( U32 i = 0; i < hashTableSize; ++ i ) for( Dictionary::Entry* entry = gEvalState.globalVars.hashTable->data[ i ]; entry != NULL; entry = entry->nextEntry ) dumpVariable( stream, entry, inClass ); } static void dumpFunction( Stream &stream, bool isClassMethod, Namespace::Entry* entry ) { String doc = entry->getDocString().trim(); String prototype = entry->getPrototypeString().trim(); // If the doc string contains @hide, skip this function. if( dStrstr( doc.c_str(), "@hide" ) || dStrstr( doc.c_str(), "@internal" ) ) return; // Make sure we have a valid function prototype. if( prototype.isEmpty() ) { Con::errorf( "Function '%s::%s' has no prototype!", entry->mNamespace->mName, entry->mFunctionName ); return; } // See if it's a static method. bool isStaticMethod = false; if( entry->mHeader ) isStaticMethod = entry->mHeader->mIsStatic; // Emit the doc comment. if( !doc.isEmpty() ) { stream.writeText( "/*!\r\n" ); // If there's no @brief, take the first line of the doc text body // as the description. const char* brief = dStrstr( doc, "@brief" ); if( !brief ) { String briefStr = entry->getBriefDescription( &doc ); briefStr.trim(); if( !briefStr.isEmpty() ) { stream.writeText( "@brief " ); stream.writeText(briefStr); stream.writeText( "\r\n\r\n" ); } } stream.writeText( doc ); // Emit @ingroup if it's not a class method. if ( !isClassMethod && !isStaticMethod ) // Extra static method check for static classes (which will come out as non-class namespaces). { const char *group = dStrstr( doc, "@ingroup" ); if( group ) { char groupName[ 256 ] = { 0 }; dSscanf( group, "@ingroup %s", groupName ); smDocGroups.insertUnique( groupName, 0 ); } #ifdef USE_UNDOCUMENTED_GROUP else { smDocGroups.insertUnique( "UNDOCUMENTED", 0 ); stream.writeText( "\r\n@ingroup UNDOCUMENTED\r\n" ); } #endif } stream.writeText( "*/\r\n" ); } #ifdef USE_UNDOCUMENTED_GROUP else if( !isClassMethod ) { smDocGroups.insertUnique( "UNDOCUMENTED", 0 ); stream.writeText( "/*! UNDOCUMENTED!\r\n@ingroup UNDOCUMENTED\r\n */\r\n" ); } #endif if( isStaticMethod ) stream.writeText( "static " ); stream.writeText( prototype ); stream.writeText( ";\r\n" ); } static void dumpNamespaceEntries( Stream &stream, Namespace *g, bool callbacks = false ) { /// Only print virtual on methods that are members of /// classes as this allows doxygen to properly do overloads. const bool isClassMethod = g->mClassRep != NULL; // Go through all the entries in the namespace. for ( Namespace::Entry *ewalk = g->mEntryList; ewalk; ewalk = ewalk->mNext ) { S32 eType = ewalk->mType; // We do not dump script defined functions... only engine exports. if( eType == Namespace::Entry::ConsoleFunctionType || eType == Namespace::Entry::GroupMarker ) continue; if( eType == Namespace::Entry::ScriptCallbackType ) { if( !callbacks ) continue; } else if( callbacks ) continue; dumpFunction( stream, isClassMethod, ewalk ); } } static void dumpClassHeader( Stream &stream, const char *usage, const char *className, const char *superClassName ) { if ( usage ) { stream.writeText( "/*!\r\n" ); stream.writeText( usage ); const char *group = dStrstr( usage, "@ingroup" ); if ( group ) { char groupName[256] = { 0 }; dSscanf( group, "@ingroup %s", groupName ); smDocGroups.insertUnique( groupName, 0 ); } #ifdef USE_UNDOCUMENTED_GROUP else { smDocGroups.insertUnique( "UNDOCUMENTED", 0 ); stream.writeText( "\r\n@ingroup UNDOCUMENTED\r\n" ); } #endif stream.writeText( "\r\n*/\r\n" ); } else { // No documentation string. Check whether ther is a separate // class doc fragement. bool haveClassDocFragment = false; if( className ) { char buffer[ 1024 ]; dSprintf( buffer, sizeof( buffer ), "@class %s", className ); for( ConsoleDocFragment* fragment = ConsoleDocFragment::smFirst; fragment != NULL; fragment = fragment->mNext ) if( !fragment->mClass && dStrstr( fragment->mText, buffer ) != NULL ) { haveClassDocFragment = true; break; } } #ifdef USE_UNDOCUMENTED_GROUP if( !haveClassDocFragment ) { smDocGroups.insertUnique( "UNDOCUMENTED", 0 ); stream.writeText( "/*! UNDOCUMENTED!\r\n@ingroup UNDOCUMENTED\r\n */\r\n" ); } #endif } // Print out appropriate class header if ( superClassName ) stream.writeText( String::ToString( "class %s : public %s {\r\npublic:\r\n", className, superClassName ? superClassName : "" ) ); else if ( className ) stream.writeText( String::ToString( "class %s {\r\npublic:\r\n", className ) ); else stream.writeText( "namespace {\r\n" ); } static void dumpClassMember( Stream &stream, const AbstractClassRep::Field& field ) { stream.writeText( "/*!\r\n" ); if( field.pFieldDocs && field.pFieldDocs[ 0 ] ) { stream.writeText( "@brief " ); String docs( field.pFieldDocs ); S32 newline = docs.find( '\n' ); if( newline == -1 ) stream.writeText( field.pFieldDocs ); else { String brief = docs.substr( 0, newline ); String body = docs.substr( newline + 1 ); stream.writeText( brief ); stream.writeText( "\r\n\r\n" ); stream.writeText( body ); } stream.writeText( "\r\n" ); } const bool isDeprecated = ( field.type == AbstractClassRep::DeprecatedFieldType ); if( isDeprecated ) stream.writeText( "@deprecated This member is deprecated and its value is always undefined.\r\n" ); stream.writeText( "*/\r\n" ); ConsoleBaseType* cbt = ConsoleBaseType::getType( field.type ); const char* type = ( cbt ? cbt->getTypeClassName() : "" ); if( field.elementCount > 1 ) stream.writeText( String::ToString( "%s %s[ %i ];\r\n", isDeprecated ? "deprecated" : type, field.pFieldname, field.elementCount ) ); else stream.writeText( String::ToString( "%s %s;\r\n", isDeprecated ? "deprecated" : type, field.pFieldname ) ); } static void dumpClassFooter( Stream &stream ) { stream.writeText( "};\r\n\r\n" ); } static void dumpGroupStart( Stream &stream, const char *aName, const char *aDocs = NULL ) { stream.writeText( String::ToString( "\r\n/*! @name %s\r\n", aName ) ); if ( aDocs ) { stream.writeText( aDocs ); stream.writeText( "\r\n" ); } stream.writeText( "@{ */\r\n" ); // Add a blank comment in order to make sure groups are parsed properly. //Con::printf(" /*! */"); } static void dumpGroupEnd( Stream &stream ) { stream.writeText( "/// @}\r\n\r\n" ); } static void dumpClasses( Stream &stream ) { Namespace::trashCache(); VectorPtr vec; vec.reserve( 1024 ); // We use mHashSequence to mark if we have traversed... // so mark all as zero to start. for ( Namespace *walk = Namespace::mNamespaceList; walk; walk = walk->mNext ) walk->mHashSequence = 0; for(Namespace *walk = Namespace::mNamespaceList; walk; walk = walk->mNext) { VectorPtr stack; stack.reserve( 1024 ); // Get all the parents of this namespace... (and mark them as we go) Namespace *parentWalk = walk; while(parentWalk) { if(parentWalk->mHashSequence != 0) break; if(parentWalk->mPackage == 0) { parentWalk->mHashSequence = 1; // Mark as traversed. stack.push_back(parentWalk); } parentWalk = parentWalk->mParent; } // Load stack into our results vector. while(stack.size()) { vec.push_back(stack[stack.size() - 1]); stack.pop_back(); } } // Go through previously discovered classes U32 i; for(i = 0; i < vec.size(); i++) { const char *className = vec[i]->mName; const char *superClassName = vec[i]->mParent ? vec[i]->mParent->mName : NULL; // Skip the global namespace, that gets dealt with in dumpFunctions if(!className) continue; // We're just dumping engine functions, then we don't want to dump // a class that only contains script functions. So, we iterate over // all the functions. bool found = false; for( Namespace::Entry *ewalk = vec[i]->mEntryList; ewalk; ewalk = ewalk->mNext ) { if( ewalk->mType != Namespace::Entry::ConsoleFunctionType ) { found = true; break; } } // If we don't have engine functions and the namespace name // doesn't match the class name... then its a script class. if ( !found && !vec[i]->isClass() ) continue; // If we hit a class with no members and no classRep, do clever filtering. if(vec[i]->mEntryList == NULL && vec[i]->mClassRep == NULL) { // Print out a short stub so we get a proper class hierarchy. if ( superClassName ) { // Filter hack; we don't want non-inheriting classes... dumpClassHeader( stream, NULL, className, superClassName ); dumpClassFooter( stream ); } continue; } // Skip over hidden or internal classes. if( vec[i]->mUsage && ( dStrstr( vec[i]->mUsage, "@hide" ) || dStrstr( vec[i]->mUsage, "@internal" ) ) ) continue; // Print the header for the class.. dumpClassHeader( stream, vec[i]->mUsage, className, superClassName ); // Dump all fragments for this class. for( ConsoleDocFragment* fragment = ConsoleDocFragment::smFirst; fragment != NULL; fragment = fragment->mNext ) if( fragment->mClass && dStricmp( fragment->mClass, className ) == 0 ) dumpFragment( stream, fragment ); // Dump member functions. dumpNamespaceEntries( stream, vec[ i ], false ); // Dump callbacks. dumpGroupStart( stream, "Callbacks" ); dumpNamespaceEntries( stream, vec[ i ], true ); dumpGroupEnd( stream ); // Dump static member variables. dumpVariables( stream, className ); // Deal with the classRep (to get members)... AbstractClassRep *rep = vec[i]->mClassRep; AbstractClassRep::FieldList emptyList; AbstractClassRep::FieldList *parentList = &emptyList; AbstractClassRep::FieldList *fieldList = &emptyList; if ( rep ) { // Get information about the parent's fields... AbstractClassRep *parentRep = vec[i]->mParent ? vec[i]->mParent->mClassRep : NULL; if(parentRep) parentList = &(parentRep->mFieldList); // Get information about our fields fieldList = &(rep->mFieldList); // Go through all our fields... for(U32 j = 0; j < fieldList->size(); j++) { const AbstractClassRep::Field &field = (*fieldList)[j]; switch( field.type ) { case AbstractClassRep::StartArrayFieldType: case AbstractClassRep::EndArrayFieldType: break; case AbstractClassRep::StartGroupFieldType: dumpGroupStart( stream, field.pGroupname, field.pFieldDocs ); break; case AbstractClassRep::EndGroupFieldType: dumpGroupEnd( stream ); break; default: case AbstractClassRep::DeprecatedFieldType: // Skip over fields that are already defined in // our parent class. if ( parentRep && parentRep->findField( field.pFieldname ) ) continue; dumpClassMember( stream, field ); break; } } } // Close the class/namespace. dumpClassFooter( stream ); } } static void dumpEnum( Stream& stream, const EngineTypeInfo* type ) { if( !type->getEnumTable() ) // Sanity check. return; // Skip internals... don't export them. if ( type->getDocString() && ( dStrstr( type->getDocString(), "@hide" ) || dStrstr( type->getDocString(), "@internal" ) ) ) return; // Write documentation. stream.writeText( "/*!\r\n" ); dumpDoc( stream, type->getDocString() ); stream.writeText( "*/\r\n" ); // Write definition. stream.writeText( "enum " ); stream.writeText( type->getTypeName() ); stream.writeText( " {\r\n" ); const EngineEnumTable& table = *( type->getEnumTable() ); const U32 numValues = table.getNumValues(); for( U32 i = 0; i < numValues; ++ i ) { const EngineEnumTable::Value& value = table[ i ]; stream.writeText( "/*!\r\n" ); dumpDoc( stream, value.getDocString(), false ); stream.writeText( "*/\r\n" ); stream.writeText( value.getName() ); stream.writeText( ",\r\n" ); } stream.writeText( "};\r\n" ); } static void dumpEnums( Stream& stream ) { for( const EngineTypeInfo* type = EngineTypeInfo::getFirstType(); type != NULL; type = type->getNextType() ) if( type->isEnum() || type->isBitfield() ) dumpEnum( stream, type ); } static bool dumpEngineDocs( const char *outputFile ) { // Create the output stream. FileStream stream; if ( !stream.open( outputFile, Torque::FS::File::Write ) ) { Con::errorf( "dumpEngineDocs - Failed to open output file." ); return false; } // First dump all global ConsoleDoc fragments. for( ConsoleDocFragment* fragment = ConsoleDocFragment::smFirst; fragment != NULL; fragment = fragment->mNext ) if( !fragment->mClass ) dumpFragment( stream, fragment ); // Clear the doc groups before continuing, smDocGroups.clear(); // Dump enumeration types. dumpEnums( stream ); // Dump all global variables. dumpVariables( stream ); // Now dump the global functions. Namespace *g = Namespace::find( NULL ); while( g ) { dumpNamespaceEntries( stream, g ); // Dump callbacks. dumpGroupStart( stream, "Callbacks" ); dumpNamespaceEntries( stream, g, true ); dumpGroupEnd( stream ); g = g->mParent; } // Now dump all the classes. dumpClasses( stream ); // Dump pre-declarations for any groups we encountered // so that we don't have to explicitly define them. HashTable::Iterator iter = smDocGroups.begin(); for (; iter != smDocGroups.end(); ++iter) stream.writeText( String::ToString( "/*! @addtogroup %s */\r\n\r\n", iter->key.c_str() ) ); return true; } DefineEngineFunction( dumpEngineDocs, bool, ( const char* outputFile ),, "Dumps the engine scripting documentation to the specified file overwriting any existing content.\n" "@param outputFile The relative or absolute output file path and name.\n" "@return Returns true if successful.\n" "@ingroup Console") { return dumpEngineDocs( outputFile ); }