Part of MsWindowsResourceLint. Save this as rcLint.h:
// this program parses RC files (as VC++ outputs them) // and reports on CUA style issues #pragma warning(disable: 4786) #include <string> #include <map> #include <assert.h> #include <iomanip> #include <vector> #include <sstream> #include <iostream> using std::cout; using std::endl; using std::ostream; using std::string; typedef std::map< string, string > strings_t; typedef std::vector<string> spec_t; typedef string::size_type size_type; class Source { public: Source(string const & rc = ""): m_rc(rc), m_bot(0), m_eot(0) {} void setResource(string const & rc) { m_rc = rc; } size_type getBOT() { return m_bot; } string const & getPriorToken() { return m_priorToken; } string const & getCurrentToken() { return m_currentToken; } string const & pullNextToken() { m_priorToken = m_currentToken; extractNextToken(); return m_currentToken; } size_type getLineNumber(size_type at) { size_type lineNumber = 1; for(size_type idx(0); idx < at; ++idx) if ('\n' == m_rc[idx]) ++lineNumber; return lineNumber; } string getLine(size_type at) { size_type bol = m_rc.rfind('\n', at); if (string::npos == bol) bol = 0; else ++bol; size_type eol = m_rc.find('\n', at); if (string::npos == eol) eol = m_rc.length(); else ++eol; return m_rc.substr(bol, eol - bol); } private: string const & extractNextToken() { char static const delims[] = " \t\n,"; m_bot = m_rc.find_first_not_of(delims, m_eot); if (string::npos == m_bot) m_currentToken = ""; else if (m_rc[m_bot] == '"') m_currentToken = parseString(); else if (m_rc.substr(m_bot, 2) == "//") { if (skipUntil("\n")) return extractNextToken(); } else if (m_rc.substr(m_bot, 2) == "/*") { if (skipUntil("*/")) return extractNextToken(); } /* else if (m_rc.substr(m_bot, 1) == "#") { string line = getLine(m_bot); size_type at(0); while(isspace(line[at]) && at < line.size()) ++at; if ('#' == line[at]) { m_eot = m_bot + 1; if (skipUntil("\n")) return extractNextToken(); } }*/ else { m_eot = m_rc.find_first_of(" \n,/", m_bot); m_currentToken = m_rc.substr(m_bot, m_eot - m_bot); } if ('#' == m_currentToken[0]) { // assert(m_rc.substr(m_bot, 1) == "#"); string line = getLine(m_bot); size_type at(0); while(isspace(line[at]) && at < line.size()) ++at; if ('#' == line[at]) { --m_eot; if (skipUntil("\n")) return extractNextToken(); } } return m_currentToken; } bool skipUntil(char const * delimiter) { m_eot = m_rc.find(delimiter, m_eot + 1); if (string::npos == m_eot) { m_currentToken = ""; return false; } m_eot += strlen(delimiter); return true; } char parseStringChar() { if (m_rc[m_eot] == '\\') { m_eot += 1; char escapee(m_rc[m_eot++]); switch (escapee) { case 'n' : return '\n'; case 'r' : return '\r'; case 't' : return '\t'; case '0' : return '\0'; case '\\': return '\\'; case 'a' : return '\a'; default : // TODO \x, \v \b, \f if (isdigit(escapee)) { string slug = m_rc.substr(m_eot - 1, 3); return char(strtol(slug.c_str(), NULL, 8)); } else //assert(false); return escapee; } } else if (m_rc[m_eot] == '"' && m_rc[m_eot+1] == '"') m_eot++; return m_rc[m_eot++]; } string parseString() { m_eot = m_bot + 1; string z; while ( m_eot < m_rc.length() && ( m_rc[m_eot] != '"' || m_rc[m_eot + 1] == '"' ) ) z += parseStringChar(); if (m_eot < m_rc.length()) m_eot += 1; return z; } string m_rc; size_type m_bot; size_type m_eot; string m_priorToken; string m_currentToken; }; class Resource; class ResourceHandle? { public: // don't read this it's just a sharing smart pointer... ResourceHandle?(Resource *p = NULL): m_pInt(new int(p != NULL)), m_p(p) {} Resource *get() { return m_p; } Resource *operator->() { return m_p; } ResourceHandle?(ResourceHandle? const & rh): m_pInt(rh.m_pInt), m_p(rh.m_p) { ++(*m_pInt); } ~ResourceHandle?(); private: Resource * m_p; int * m_pInt; }; class ComplaintDepartment? { public: virtual string getLine(size_type at) = 0; virtual size_type getLineNumber(size_type at) = 0; virtual void complain(string const & description, size_type bot, string evidence = "") = 0; virtual string getString(string const & id) = 0; virtual Resource * getAccelerators(string const & menuID) = 0; }; // DependencyInversionPrinciple class Resource { public: void setBeginningOfText(size_type bot) { m_bot = bot; } string getID() { return getSpec(0); } virtual Resource * clone() { return new Resource(*this); } virtual Resource * get(string /*id*/) { return NULL; } virtual Resource * get(int /*idx*/) { return NULL; } virtual Resource * get_(int /*idx*/) { return NULL; } virtual void parse(Source & /*aSource*/) { } virtual string getLabel() { return getSpec(getLabelIndex()); } virtual void LintOne?(ComplaintDepartment? & ) {} virtual void LintAll?(ComplaintDepartment? & ) {} size_type getBeginningOfText() { return m_bot; } int getSpecCount() const { return m_spec.size(); } string const & getSpec(int x) const { return m_spec[x]; } void addSpec(string const & token) { m_spec.push_back(token); } virtual bool weBePromptLabel() { return false; } char getLabelHotkey() { string label = getLabel(); size_type at = label.find('&'); if ( string::npos != at && (at + 2) < label.length() && '&' != label[at + 1] ) return label[at + 1]; return '\0'; } virtual void printTree(ostream & out, int depth = 1) { out << std::setw(depth) << " " << getID() << endl; } void LintLabelPrompt?(ComplaintDepartment? & aCD) // TODO oaoo? { char hotKey = this->getLabelHotkey(); if (!hotKey) aCD.complain("Missing & in Menu Label", m_bot); // TODO make m_bot private } private: virtual int getLabelIndex() { return 1; } spec_t m_spec; size_type m_bot; }; class // TODO or Accelerator Control: public Resource { public: bool weBePromptLabel() { return "LTEXT" == getSpec(1) && ':' == *getLabel().rbegin(); } private: virtual int getLabelIndex() { return 2; } Resource * clone() { return new Control(*this); } bool weBeButton() { return "CONTROL" == getSpec(1) && "Button" == getSpec(4); } bool weBePushButton() { return ( "PUSHBUTTON" == getSpec(1) || "DEFPUSHBUTTON" == getSpec(1) ) && "IDOK" != getSpec(3) && "IDCANCEL" != getSpec(3); } virtual void LintOne?(ComplaintDepartment? & aCD) { // TODO put quotes back on strings if ( weBeButton() || weBePromptLabel() || weBePushButton() ) { // cout << getLabel() << endl; if (!getLabelHotkey()) aCD.complain("Button missing hotkey", getBeginningOfText()); } } }; string toString(int i) { std::stringstream z; z << i; return z.str(); } class ResourceCollection?: public Resource { public: void add(Resource & nu) { m_Resources[nu.getID()] = nu.clone(); } void printTree(ostream & out, int depth = 1) { Resource::printTree(out, depth); for( Resources_t::iterator it = m_Resources.begin(); it != m_Resources.end(); ++it ) it->second->printTree(out, depth + 5); } // TODO this merged with LintAll?() == Visitor int getCount() { return m_Resources.size(); } Resource * get(string id) { Resources_t::iterator it = m_Resources.find(id); if (it == m_Resources.end()) return NULL; return it->second.operator->(); } Resource * get(int idx) { Resources_t::iterator it = m_Resources.begin(); while (it != m_Resources.end() && idx--) ++it; return it != m_Resources.end()? it->second.operator->(): NULL; } Resource * get_(int idx) { return get(toString(idx));/* Resources_t::iterator it = m_Resources.begin(); while (it != m_Resources.end() && idx--) ++it; return it != m_Resources.end()? it->second.operator->(): NULL;*/ } virtual string parseName(Source & /*aSource*/) { return ""; } void parseResource(ResourceCollection? & aSink, Source & aSource) { setBeginningOfText(aSource.getBOT()); addSpec(parseName(aSource)); string begin; do { begin = aSource.pullNextToken(); if (begin == "") return; } while("BEGIN" != begin); aSource.pullNextToken(); for(;;) { string token = aSource.getCurrentToken(); if ("" == token || "END" == token) break; parse(aSource); } aSink.add(*this); } void LintAll?(ComplaintDepartment? & aCD) { for( Resources_t::iterator it = m_Resources.begin(); it != m_Resources.end(); ++it ) { it->second->LintOne?(aCD); it->second->LintAll?(aCD); } } void LintDupedLabelHotKeys?(ComplaintDepartment? & aCD, string type) { Resource * pControl = NULL; for ( int idx(0); pControl = get(idx); ++idx ) if (char hotKey = pControl->getLabelHotkey()) { string evidence; Resource * pControl2 = NULL; for ( int idx2(idx + 1); pControl2 = get(idx2); ++idx2 ) // TODO OAOO { char hotKey2 = pControl2->getLabelHotkey(); if (hotKey == hotKey2) evidence += aCD.getLine(pControl2->getBeginningOfText()); } if ("" != evidence) aCD.complain( "Duplicated " + type + " hotkey", pControl->getBeginningOfText(), evidence ); } } virtual bool isFirstTokenOfNextItem(string const & token) { return false; } void initResource(Resource & aResource, Source & aSource) { aResource.addSpec(toString(getCount())); aResource.addSpec(aSource.getCurrentToken()); aResource.setBeginningOfText(aSource.getBOT()); string token = aSource.pullNextToken(); while (token != "END") { if ( isFirstTokenOfNextItem(token) ) break; aResource.addSpec(token); token = aSource.pullNextToken(); } add(aResource); } private: typedef std::map< string, ResourceHandle? > Resources_t; Resources_t m_Resources; }; ResourceHandle?::~ResourceHandle?() // permits derived Resource objects to occupy std::map<> { --(*m_pInt); if(!m_pInt) { delete m_p; delete m_pInt; } } class MenuHierarchy?: public ResourceCollection? { public: void setMenuID(string const & nu) { m_MenuID = nu; } string const & getMenuID() { return m_MenuID; } virtual void addMenuItem(Source & aSource); void parse(Source & aSource); private: string m_MenuID; }; class // also string in STRINGTABLE MenuItem?: public MenuHierarchy? { public: private: Resource * clone() { return new MenuItem?(*this); } void complainAbout(ComplaintDepartment? & aCD, char const * description, Resource & aResource) { aCD.complain ( description, getBeginningOfText(), aCD.getLine(aResource.getBeginningOfText()) ); } void LintAccelerator?(ComplaintDepartment? & aCD, Resource & aControl) { if (getID() == aControl.getSpec(2)) // TODO make getLabel get the label { size_type at = getLabel().find('\t'); if (string::npos == at) complainAbout(aCD, "Missing accelerator prompt in Menu Label", aControl); else { string slice = getLabel().substr(at); string keyCap = slice.substr(slice.length() - 1, 1); if (keyCap != aControl.getSpec(1)) complainAbout(aCD, "Wrong accelerator prompt in Menu Label", aControl); } } } void LintAccelerators?(ComplaintDepartment? & aCD, Resource & anAccelerator) { for(int idx(0);;++idx) { Resource * pControl = anAccelerator.get(idx); if (!pControl) break; LintAccelerator?(aCD, *pControl); } } void LintOne?(ComplaintDepartment? & aCD) { LintLabelPrompt?(aCD); string help = aCD.getString(getID()); if ("" == help) aCD.complain("Missing StringTable? help for MenuItem?", this->getBeginningOfText()); Resource * p = aCD.getAccelerators(getMenuID()); if (p) LintAccelerators?(aCD, *p); } }; class Popup: public MenuHierarchy? { public: Resource *clone() { return new Popup(*this); } string parseName(Source & aSource) { return aSource.pullNextToken(); } private: void LintOne?(ComplaintDepartment? & aCD) { LintLabelPrompt?(aCD); LintDupedLabelHotKeys?(aCD, "MenuItem?"); } private: virtual int getLabelIndex() { return 0; } }; void MenuHierarchy?::addMenuItem(Source & aSource) { string label = aSource.pullNextToken(); assert(label.size()); if ("SEPARATOR" == label) { aSource.pullNextToken(); return; } size_type bot = aSource.getBOT(); // TODO getBeginningOfText() string id = aSource.pullNextToken(); MenuItem? anItem; anItem.setBeginningOfText(bot); anItem.addSpec(id); anItem.addSpec(label); anItem.setMenuID(getMenuID()); string token; for (;;) { token = aSource.pullNextToken(); if ( "END" == token || "POPUP" == token || "MENUITEM" == token) break; anItem.addSpec(token); } add(anItem); } void MenuHierarchy?::parse(Source & aSource) { string token = aSource.getCurrentToken(); if ("MENUITEM" == token) addMenuItem(aSource); else if ("POPUP" == token) { Popup aPopup; aPopup.setMenuID(getMenuID()); aPopup.parseResource(*this, aSource); token = aSource.pullNextToken(); } else assert(false); // syntax error in your resource code! Look at token and m_bot } class Menu: public MenuHierarchy? { public: Resource *clone() { return new Menu(*this); } string parseName(Source & aSource) { string token = aSource.getPriorToken(); // TODO commonalize this setMenuID(token); return token; } }; class StringTable?: public ResourceCollection? { public: StringTable?(int idx): m_idx(toString(idx)) {} private: string parseName(Source & aSource) { return m_idx; } void parse(Source & aSource) { string token = aSource.getCurrentToken(); Resource anEntry; anEntry.setBeginningOfText(aSource.getBOT()); anEntry.addSpec(token); anEntry.addSpec(aSource.pullNextToken()); add(anEntry); aSource.pullNextToken(); } Resource *clone() { return new StringTable?(*this); } string m_idx; }; class Accelerators: public ResourceCollection? { Resource *clone() { return new Accelerators(*this); } string parseName(Source & aSource) { return aSource.getPriorToken() + " ACCELERATORS"; } bool isFirstTokenOfNextItem(string const & token) { return (1 == token.length() && "|" != token) || "VK_" == token.substr(0, 3); } void parse(Source & aSource) { Control aCtrl; initResource(aCtrl, aSource); } }; class Dialog: public ResourceCollection? { public: Resource *clone() { return new Dialog(*this); } string parseName(Source & aSource) // get prior name class?? TODO { return aSource.getPriorToken(); } void LintPromptLabel?(ComplaintDepartment? & aCD, Resource & aResource, int idx) { if (Resource * pNext = get(toString(idx + 1))) { bool found (false); for(int x(0); x < pNext->getSpecCount(); ++x) { if ("WS_TABSTOP" == pNext->getSpec(x)) { found = true; break; } } if (!found) aCD.complain( "Prompt before control without tabstop", aResource.getBeginningOfText() ); } else { aCD.complain( "Prompt at end of control list", aResource.getBeginningOfText() ); } } void LintOne?(ComplaintDepartment? & aCD) { LintDupedLabelHotKeys?(aCD, "Control"); Resource * pResource = NULL; for ( int idx(0); pResource = get(toString(idx)); ++idx ) if (pResource->weBePromptLabel()) LintPromptLabel?(aCD, *pResource, idx); } bool isFirstTokenOfNextItem(string const & token) { return token == "LTEXT" || token == "RTEXT" || token == "DEFPUSHBUTTON" || token == "PUSHBUTTON" || token == "AUTO3STATE" || token == "AUTOCHECKBOX" || token == "AUTORADIOBUTTON" || token == "CHECKBOX" || token == "COMBOBOX" || token == "CONTROL" || token == "CTEXT" || token == "EDITTEXT" || token == "GROUPBOX" || token == "ICON" || token == "LISTBOX" || token == "PUSHBOX" || token == "RADIOBUTTON" || token == "SCROLLBAR" || token == "STATE3"; } void parse(Source & aSource) { Control aCtrl; initResource(aCtrl, aSource); } }; class ResourceFile? : public ResourceCollection?, public ComplaintDepartment? { public: Resource *clone() { return NULL; } // the buck stops here ResourceFile?(): m_pComplaints(NULL) {} virtual size_type getLineNumber(size_type at) { return m_Source.getLineNumber(at); } virtual string getLine(size_type at) { return m_Source.getLine(at); } string getString(string const & id) { int idx(0); for(;;) { Resource * p = get(toString(idx++)); if (!p) return ""; p = p->get(id); if (p) return p->getLabel(); } } void callLint(ostream & complaints) { m_pComplaints = &complaints; LintAll?(*this); m_pComplaints = NULL; } Resource * getAccelerators(string const & menuID) { return get(menuID + " ACCELERATORS"); } void complain(string const & description, size_type bot, string evidence = "") { size_type lineNumber(getLineNumber(bot)); *m_pComplaints << getID() << '(' << lineNumber << "): " << description << "\n"; *m_pComplaints << getLine(bot); if ("" != evidence) *m_pComplaints << evidence; *m_pComplaints << "\n"; } void parseFile( string const & rc, string const & fileName = "" ) { m_Source.setResource(rc); setBeginningOfText(m_Source.getBOT()); addSpec(fileName); int idx(0); for(;;) { string token = m_Source.pullNextToken(); if ("" == token) return; if ("MENU" == token) { Menu().parseResource(*this, m_Source); } else if ("DIALOG" == token || "DIALOGEX" == token) { Dialog().parseResource(*this, m_Source); } else if ("ACCELERATORS" == token) { Accelerators().parseResource(*this, m_Source); } else if ("STRINGTABLE" == token) { StringTable?(idx++).parseResource(*this, m_Source); } else { // std::cerr << "Unrecognized: " << token << endl; } } } // TODO everyone deals with END the same way. private: Source m_Source; ostream * m_pComplaints; };