Ms Windows Resource Lint Header

last modified: April 19, 2011

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;
    },;

CategoryLint


Loading...