Modules | Files | Inheritance Tree | Inheritance Graph | Name Index | Config
File: Synopsis/Linker/Comments.py
    1| # $Id: Comments.py,v 1.18 2003/01/20 06:43:02 chalky Exp $
    2| #
    3| # This file is a part of Synopsis.
    4| # Copyright (C) 2000, 2001 Stephen Davies
    5| # Copyright (C) 2000, 2001 Stefan Seefeld
    6| #
    7| # Synopsis is free software; you can redistribute it and/or modify it
    8| # under the terms of the GNU General Public License as published by
    9| # the Free Software Foundation; either version 2 of the License, or
   10| # (at your option) any later version.
   11| #
   12| # This program is distributed in the hope that it will be useful,
   13| # but WITHOUT ANY WARRANTY; without even the implied warranty of
   14| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   15| # General Public License for more details.
   16| #
   17| # You should have received a copy of the GNU General Public License
   18| # along with this program; if not, write to the Free Software
   19| # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
   20| # 02111-1307, USA.
   21| #
   22| # $Log: Comments.py,v $
   23| # Revision 1.18  2003/01/20 06:43:02  chalky
   24| # Refactored comment processing. Added AST.CommentTag. Linker now determines
   25| # comment summary and extracts tags. Increased AST version number.
   26| #
   27| # Revision 1.17  2002/10/11 11:07:53  chalky
   28| # Added missing parent __init__ call in Group
   29| #
   30| # Revision 1.16  2002/10/11 05:57:17  chalky
   31| # Support suspect comments
   32| #
   33| # Revision 1.15  2002/09/20 10:34:52  chalky
   34| # Add a comment parser for plain old // comments
   35| #
   36| # Revision 1.14  2002/08/23 04:37:26  chalky
   37| # Huge refactoring of Linker to make it modular, and use a config system similar
   38| # to the HTML package
   39| #
   40| # Revision 1.13  2002/04/26 01:21:14  chalky
   41| # Bugs and cleanups
   42| #
   43| # Revision 1.12  2002/04/25 23:54:09  chalky
   44| # Fixed bug caused by new re module in python 2.1 handling groups differently
   45| #
   46| # Revision 1.11  2001/06/11 10:37:49  chalky
   47| # Better grouping support
   48| #
   49| # Revision 1.10  2001/06/08 21:04:38  stefan
   50| # more work on grouping
   51| #
   52| # Revision 1.9  2001/06/08 04:50:13  stefan
   53| # add grouping support
   54| #
   55| # Revision 1.8  2001/05/25 13:45:49  stefan
   56| # fix problem with getopt error reporting
   57| #
   58| # Revision 1.7  2001/04/05 16:21:56  chalky
   59| # Allow user-specified comment processors
   60| #
   61| # Revision 1.6  2001/04/05 11:11:39  chalky
   62| # Many more comments
   63| #
   64| # Revision 1.5  2001/02/12 04:08:09  chalky
   65| # Added config options to HTML and Linker. Config demo has doxy and synopsis styles.
   66| #
   67| # Revision 1.4  2001/02/07 17:00:43  chalky
   68| # Added Qt-style comments support
   69| #
   70| # Revision 1.3  2001/02/07 14:13:51  chalky
   71| # Small fixes.
   72| #
   73| # Revision 1.2  2001/02/07 09:57:00  chalky
   74| # Support for "previous comments" in C++ parser and Comments linker.
   75| #
   76| # Revision 1.1  2001/02/06 06:55:18  chalky
   77| # Initial commit. Support SSD and Java comments. Selection of comments only
   78| # (same as ssd,java formatters in HTML)
   79| #
   80| #
   81| 
   82| """Comment Processor"""
   83| # System modules
   84| import sys, string, re, getopt, types
   85| 
   86| # Synopsis modules
   87| from Synopsis.Core import AST, Util
   88| 
   89| from Synopsis.Linker.Linker import config, Operation
   90| 
   91| class CommentProcessor (AST.Visitor):
   92|     """Base class for comment processors.
   93| 
   94|     This is an AST visitor, and by default all declarations call process()
   95|     with the current declaration. Subclasses may override just the process
   96|     method.
   97|     """
   98|     def processAll(self, declarations):
   99|         for decl in declarations:
  100|             decl.accept(self)
  101|     def process(self, decl):
  102|         """Process comments for the given declaration"""
  103|     def visitDeclaration(self, decl):
  104|         self.process(decl)
  105| 
  106| class SSDComments (CommentProcessor):
  107|     """A class that selects only //. comments."""
  108|     __re_star = r'/\*(.*?)\*/'
  109|     __re_ssd = r'^[ \t]*//\. ?(.*)$'
  110|     def __init__(self):
  111|         "Compiles the regular expressions"
  112|         self.re_star = re.compile(SSDComments.__re_star, re.S)
  113|         self.re_ssd = re.compile(SSDComments.__re_ssd, re.M)
  114|     def process(self, decl):
  115|         "Calls processComment on all comments"
  116|         map(self.processComment, decl.comments())
  117|     def processComment(self, comment):
  118|         """Replaces the text in the comment. It calls strip_star() first to
  119|         remove all multi-line star comments, then follows with parse_ssd().
  120|         """
  121|         text = comment.text()
  122|         text = self.parse_ssd(self.strip_star(text))
  123|         comment.set_text(text)
  124|     def strip_star(self, str):
  125|         """Strips all star-format comments from the string"""
  126|         mo = self.re_star.search(str)
  127|         while mo:
  128|             str = str[:mo.start()] + str[mo.end():]
  129|             mo = self.re_star.search(str)
  130|         return str
  131|     def parse_ssd(self, str):
  132|         """Filters str and returns just the lines that start with //."""
  133|         return string.join(self.re_ssd.findall(str),'\n')
  134| 
  135| class JavaComments (CommentProcessor):
  136|     """A class that formats java /** style comments"""
  137|     __re_java = r"/\*\*[ \t]*(?P<text>.*)(?P<lines>(\n[ \t]*\*.*)*?)(\n[ \t]*)?\*/"
  138|     __re_line = r"\n[ \t]*\*[ \t]*(?P<text>.*)"
  139|     def __init__(self):
  140|         "Compiles the regular expressions"
  141|         self.re_java = re.compile(JavaComments.__re_java)
  142|         self.re_line = re.compile(JavaComments.__re_line)
  143|     def process(self, decl):
  144|         "Calls processComment on all comments"
  145|         map(self.processComment, decl.comments())
  146|     def processComment(self, comment):
  147|         """Finds comments in the java format. The format is  /** ... */, and
  148|         it has to cater for all four line forms: "/** ...", " * ...", " */" and
  149|         the one-line "/** ... */".
  150|         """
  151|         text = comment.text()
  152|         text_list = []
  153|         mo = self.re_java.search(text)
  154|         while mo:
  155|             text_list.append(mo.group('text'))
  156|             lines = mo.group('lines')
  157|             if lines:
  158|                mol = self.re_line.search(lines)
  159|                while mol:
  160|                    text_list.append(mol.group('text'))
  161|                    mol = self.re_line.search(lines, mol.end())
  162|             mo = self.re_java.search(text, mo.end())
  163|         text = string.join(text_list,'\n')
  164|         comment.set_text(text)
  165| 
  166| class SSComments (CommentProcessor):
  167|     """A class that selects only // comments."""
  168|     __re_star = r'/\*(.*?)\*/'
  169|     __re_ss = r'^[ \t]*// ?(.*)$'
  170|     def __init__(self):
  171|         "Compiles the regular expressions"
  172|         self.re_star = re.compile(SSComments.__re_star, re.S)
  173|         self.re_ss = re.compile(SSComments.__re_ss, re.M)
  174|     def process(self, decl):
  175|         "Calls processComment on all comments"
  176|         map(self.processComment, decl.comments())
  177|     def processComment(self, comment):
  178|         """Replaces the text in the comment. It calls strip_star() first to
  179|         remove all multi-line star comments, then follows with parse_ss().
  180|         """
  181|         text = comment.text()
  182|         text = self.parse_ss(self.strip_star(text))
  183|         comment.set_text(text)
  184|     def strip_star(self, str):
  185|         """Strips all star-format comments from the string"""
  186|         mo = self.re_star.search(str)
  187|         while mo:
  188|             str = str[:mo.start()] + str[mo.end():]
  189|             mo = self.re_star.search(str)
  190|         return str
  191|     def parse_ss(self, str):
  192|         """Filters str and returns just the lines that start with //"""
  193|         return string.join(self.re_ss.findall(str),'\n')
  194| 
  195| 
  196| class QtComments (CommentProcessor):
  197|     """A class that finds Qt style comments. These have two styles: //! ...
  198|     and /*! ... */. The first means "brief comment" and there must only be
  199|     one. The second type is the detailed comment."""
  200|     __re_brief = r"[ \t]*//!(.*)"
  201|     __re_detail = r"[ \t]*/\*!(.*)\*/[ \t\n]*"
  202|     def __init__(self):
  203|         "Compiles the regular expressions"
  204|         self.re_brief = re.compile(self.__re_brief)
  205|         self.re_detail = re.compile(self.__re_detail, re.S)
  206|     def process(self, decl):
  207|         "Calls processComment on all comments"
  208|         map(self.processComment, decl.comments())
  209|     def processComment(self, comment):
  210|         "Matches either brief or detailed comments"
  211|         text = comment.text()
  212|         mo = self.re_brief.match(text)
  213|         if mo:
  214|             comment.set_text(mo.group(1))
  215|           return
  216|         mo = self.re_detail.match(text)
  217|         if mo:
  218|             comment.set_text(mo.group(1))
  219|           return
  220|         comment.set_text('')
  221| 
  222| class Transformer (CommentProcessor):
  223|     """A class that creates a new AST from an old one. This is a helper base for
  224|     more specialized classes that manipulate the AST based on the comments in the nodes"""
  225|     def __init__(self):
  226|         """Constructor"""
  227|         self.__scopestack = []
  228|         self.__currscope = []
  229|     def processAll(self, declarations):
  230|         """Overrides the default processAll() to setup the stack"""
  231|         for decl in declarations: decl.accept(self)
  232|         declarations[:] = self.__currscope
  233|     def push(self):
  234|         """Pushes the current scope onto the stack and starts a new one"""
  235|         self.__scopestack.append(self.__currscope)
  236|         self.__currscope = []
  237|     def pop(self, decl):
  238|         """Pops the current scope from the stack, and appends the given
  239|         declaration to it"""
  240|         self.__currscope = self.__scopestack.pop()
  241|         self.__currscope.append(decl)
  242|     def add(self, decl):
  243|         """Adds the given decl to the current scope"""
  244|         self.__currscope.append(decl)
  245|     def currscope(self):
  246|         """Returns the current scope: a list of declarations"""
  247|         return self.__currscope
  248|         
  249| class Dummies (Transformer):
  250|     """A class that deals with dummy declarations and their comments. This
  251|     class just removes them."""
  252|     def visitDeclaration(self, decl):
  253|         """Checks for dummy declarations"""
  254|         if decl.type() == "dummy": return
  255|         self.add(decl)
  256|     def visitScope(self, scope):
  257|         """Visits all children of the scope in a new scope. The value of
  258|         currscope() at the end of the list is used to replace scope's list of
  259|         declarations - hence you can remove (or insert) declarations from the
  260|         list. Such as dummy declarations :)"""
  261|         self.push()
  262|         for decl in scope.declarations(): decl.accept(self)
  263|         scope.declarations()[:] = self.currscope()
  264|         self.pop(scope)
  265|     def visitEnum(self, enum):
  266|         """Does the same as visitScope, but for the enum's list of
  267|         enumerators"""
  268|         self.push()
  269|         for enumor in enum.enumerators(): enumor.accept(self)
  270|         enum.enumerators()[:] = self.currscope()
  271|         self.pop(enum)
  272|     def visitEnumerator(self, enumor):
  273|         """Removes dummy enumerators"""
  274|         if enumor.type() == "dummy": return #This wont work since Core.AST.Enumerator forces type to "enumerator"
  275|         if not len(enumor.name()): return # workaround.
  276|         self.add(enumor)
  277| 
  278| class Previous (Dummies):
  279|     """A class that maps comments that begin with '<' to the previous
  280|     declaration"""
  281|     def processAll(self, declarations):
  282|         """decorates processAll() to initialise last and laststack"""
  283|         self.last = None
  284|         self.__laststack = []
  285|         for decl in declarations:
  286|             decl.accept(self)
  287|             self.last = decl
  288|         declarations[:] = self.currscope()
  289|     def push(self):
  290|         """decorates push() to also push 'last' onto 'laststack'"""
  291|         Dummies.push(self)
  292|         self.__laststack.append(self.last)
  293|         self.last = None
  294|     def pop(self, decl):
  295|         """decorates pop() to also pop 'last' from 'laststack'"""
  296|         Dummies.pop(self, decl)
  297|         self.last = self.__laststack.pop()
  298|     def visitScope(self, scope):
  299|         """overrides visitScope() to set 'last' after each declaration"""
  300|         self.removeSuspect(scope)
  301|         self.push()
  302|         for decl in scope.declarations():
  303|             decl.accept(self)
  304|             self.last = decl
  305|         scope.declarations()[:] = self.currscope()
  306|         self.pop(scope)
  307|     def checkPrevious(self, decl):
  308|         """Checks a decl to see if the comment should be moved. If the comment
  309|         begins with a less-than sign, then it is moved to the 'last'
  310|         declaration"""
  311|         if len(decl.comments()) and self.last:
  312|             first = decl.comments()[0]
  313|             if len(first.text()) and first.text()[0] == "<" and self.last:
  314|                first.set_suspect(0) # Remove suspect flag
  315|                first.set_text(first.text()[1:]) # Remove '<'
  316|                self.last.comments().append(first)
  317|                del decl.comments()[0]
  318|     def removeSuspect(self, decl):
  319|         """Removes any suspect comments from the declaration"""
  320|         non_suspect = lambda decl: not decl.is_suspect()
  321|         comments = decl.comments()
  322|         comments[:] = filter(non_suspect, comments)
  323|     def visitDeclaration(self, decl):
  324|         """Calls checkPrevious on the declaration and removes dummies"""
  325|         self.checkPrevious(decl)
  326|         self.removeSuspect(decl)
  327|         if decl.type() != "dummy":
  328|             self.add(decl)
  329|     def visitEnum(self, enum):
  330|         """Does the same as visitScope but for enum and enumerators"""
  331|         self.removeSuspect(enum)
  332|         self.push()
  333|         for enumor in enum.enumerators():
  334|             enumor.accept(self)
  335|             self.last = enumor
  336|         enum.enumerators()[:] = self.currscope()
  337|         self.pop(enum)
  338|     def visitEnumerator(self, enumor):
  339|         """Checks previous comment and removes dummies"""
  340|         self.removeSuspect(enumor)
  341|         self.checkPrevious(enumor)
  342|         if len(enumor.name()): self.add(enumor)
  343| 
  344| class Grouper (Transformer):
  345|     """A class that detects grouping tags and moves the enclosed nodes into a subnode (a 'Group')"""
  346|     __re_open = r'^[ \t]*{ ?(.*)$'
  347|     __re_close = r'^[ \t]*} ?(.*)$'
  348|     def __init__(self):
  349|         Transformer.__init__(self)
  350|         self.re_open = re.compile(Grouper.__re_open, re.M)
  351|         self.re_close = re.compile(Grouper.__re_close, re.M)
  352|         self.__groups = []
  353|     def visitDeclaration(self, decl):
  354|         """Checks for grouping tags.
  355|         If an opening tag is found in the middle of a comment, a new Group is generated, the preceeding
  356|         comments are associated with it, and is pushed onto the scope stack as well as the groups stack.
  357|         """
  358|         comments = []
  359|         process_comments = decl.comments()
  360|         while len(process_comments):
  361|             c = process_comments.pop(0)
  362|             open_mo = self.re_open.search(c.text())
  363|             if open_mo:
  364|                # Open group. Name is remainder of line
  365|                 label = open_mo.group(1)
  366|                # The comment before the { becomes the group comment
  367|                if open_mo.start() > 0:
  368|                    text = c.text()[:open_mo.start()]
  369|                    comments.append(AST.Comment(text, c.file(), c.line()))
  370|                 group = AST.Group(decl.file(), decl.line(), decl.language(), "group", [label])
  371|                 group.comments()[:] = comments
  372|                 comments = []
  373|                # The comment after the { becomes the next comment to process
  374|                if open_mo.end() < len(c.text()):
  375|                    text = c.text()[open_mo.end()+1:]
  376|                    process_comments.insert(0, AST.Comment(text, c.file(), c.line()))
  377|                 self.push()
  378|                 self.__groups.append(group)
  379|                continue
  380|             close_mo = self.re_close.search(c.text())
  381|             if close_mo:
  382|                # Fixme: the close group doesn't handle things as well as open
  383|         # does!
  384|                 group = self.__groups.pop()
  385|                 group.declarations()[:] = self.currscope()
  386|                 self.pop(group)
  387|                # The comment before the } is ignored...? maybe post-comment?
  388|                # The comment after the } becomes the next comment to process
  389|                if close_mo.end() < len(c.text()):
  390|                    text = c.text()[close_mo.end()+1:]
  391|                    process_comments.insert(0, AST.Comment(text, c.file(), c.line()))
  392|             else: comments.append(c)
  393|         decl.comments()[:] = comments
  394|         self.add(decl)
  395|     def visitScope(self, scope):
  396|         """Visits all children of the scope in a new scope. The value of
  397|         currscope() at the end of the list is used to replace scope's list of
  398|         declarations - hence you can remove (or insert) declarations from the
  399|         list. Such as dummy declarations :)"""
  400|         self.push()
  401|         for decl in scope.declarations(): decl.accept(self)
  402|         scope.declarations()[:] = self.currscope()
  403|         self.pop(scope)
  404|     def visitEnum(self, enum):
  405|         """Does the same as visitScope, but for the enum's list of
  406|         enumerators"""
  407|         self.push()
  408|         for enumor in enum.enumerators(): enumor.accept(self)
  409|         enum.enumerators()[:] = self.currscope()
  410|         self.pop(enum)
  411|     def visitEnumerator(self, enumor):
  412|         """Removes dummy enumerators"""
  413|         if enumor.type() == "dummy": return #This wont work since Core.AST.Enumerator forces type to "enumerator"
  414|         if not len(enumor.name()): return # workaround.
  415|         self.add(enumor)
  416| 
  417| class Summarizer (CommentProcessor):
  418|     """Splits comments into summary/detail parts."""
  419|     re_summary = r"[ \t\n]*(.*?\.)([ \t\n]|$)"
  420|     def __init__(self):
  421|         self.re_summary = re.compile(Summarizer.re_summary, re.S)
  422|     def process(self, decl):
  423|         """Combine and summarize the comments of this declaration."""
  424|         # First combine
  425|         comments = decl.comments()
  426|         if not len(comments):
  427|           return
  428|         comment = comments[0]
  429|         tags = comment.tags()
  430|         if len(comments) > 1:
  431|             # Should be rare to have >1 comment
  432|             for extra in comments[1:]:
  433|                tags.extend(extra.tags())
  434|                comment.set_text(comment.text() + extra.text())
  435|             del comments[1:]
  436|         # Now decide how much of the comment is the summary
  437|         text = comment.text()
  438|         mo = self.re_summary.match(text)
  439|         if mo:
  440|             # Set summary to the sentence
  441|             comment.set_summary(mo.group(1))
  442|         else:
  443|             # Set summary to whole text
  444|             comment.set_summary(text)
  445| 
  446| class JavaTags (CommentProcessor):
  447|     """Extracts javadoc-style @tags from the end of comments."""
  448| 
  449|     # The regexp to use for finding all the tags
  450|     _re_tags = '\n[ \t]*(?P<tags>@[a-zA-Z]+[ \t]+.*)'
  451| 
  452|     def __init__(self):
  453|         self.re_tags = re.compile(self._re_tags,re.M|re.S)
  454|     def process(self, decl):
  455|         """Extract tags from each comment of the given decl"""
  456|         for comment in decl.comments():
  457|             # Find tags
  458|             text = comment.text()
  459|             mo = self.re_tags.search(text)
  460|             if not mo:
  461|                continue
  462|             # A lambda to use in the reduce expression
  463|             joiner = lambda x,y: len(y) and y[0]=='@' and x+[y] or x[:-1]+[x[-1]+' '+y]
  464| 
  465|             tags = mo.group('tags')
  466|             text = text[:mo.start('tags')]
  467|             # Split the tag section into lines
  468|             tags = map(string.strip, string.split(tags,'\n'))
  469|             # Join non-tag lines to the previous tag
  470|             tags = reduce(joiner, tags, [])
  471|             # Split the tag lines into @name, rest-of-line pairs
  472|             tags = map(lambda line: string.split(line,' ',1), tags)
  473|             # Convert the pairs into CommentTag objects
  474|             tags = map(lambda pair: AST.CommentTag(pair[0], pair[1]), tags)
  475|             # Store back in comment
  476|             comment.set_text(text)
  477|             comment.tags().extend(tags)
  478| 
  479| processors = {
  480|     'ssd': SSDComments,
  481|     'ss' : SSComments,
  482|     'java': JavaComments,
  483|     'qt': QtComments,
  484|     'dummy': Dummies,
  485|     'prev': Previous,
  486|     'group': Grouper,
  487|     'summary' : Summarizer,
  488|     'javatags' : JavaTags,
  489| }
  490| 
  491| class Comments(Operation):
  492|     def __init__(self):
  493|         """Constructor, parses the config object"""
  494|         self.processor_list = []
  495| 
  496|         if hasattr(config, 'comment_processors'):
  497|             for proc in config.comment_processors:
  498|                if type(proc) == types.StringType:
  499|                    if processors.has_key(proc):
  500|                       self.processor_list.append(processors[proc]())
  501|                else:
  502|                       raise ImportError, 'No such processor: %s'%(proc,)
  503|                elif type(proc) == types.TupleType:
  504|                    mod = Util._import(proc[0])
  505|                    clas = getattr(mod, proc[1])
  506|                    self.processor_list.append(clas())
  507| 
  508|     def execute(self, ast):
  509|         declarations = ast.declarations()
  510|         for processor in self.processor_list:
  511|             processor.processAll(declarations)
  512| 
  513| linkerOperation = Comments