#!/usr/bin/env python # # wmlscope -- generate reports on WML macro and resource usage # # By Eric S. Raymond, April 2007. # # This tool cross-references macro definitions with macro calls, and # resource (sound or image) files with uses of the resources in WML. # and generates various useful reports from such cross-references. # # (Most of the work is done by a cross-referencer class that is also # used elsewhere, e.g. by wmlmove.) # # It takes a list of directories as arguments; if none is given, it # behaves as though the current directory had been specified as a # single argument. Each directory is treated as a separate domain for # macro and resource visibility purposes. # # There are two kinds of namespace, exporting and non-exporting. # Exporting namespaces make all their resources and macro names # globally visible. You can make a namespace exporting by embedding # a comment like this in it: # # # wmlscope: export=yes # # Wesnoth core data is an exporting namespace. Campaigns are non-exporting; # they should contain the declaration # # # wmlscope: export=no # # somewhere. wmlscope will complain when it sees a nam,espace with no export # property, then treat it as non-exporting. # # This tool does catch one kind of implicit reference: if an attack name # is specified but no icon is given, the attack icon will default to # a name generated from the attack name. This behavior can be suppressed # by adding a magic comment containing the string "no-icon" to the name= line. # # The checking done by this tool has a couple of flaws: # # (1) It doesn't actually evaluate file inclusions. Instead, any # macro definition satisfies any macro call made under the same # directory. Exception: when an #undef is detected, the macro is # tagged local and not visible outside the span of lines where it's # defined. # # (2) It doesn't read [binary_path] tags, as this would require # implementing a WML parser. Instead, it assumes that a resource-file # reference can be satisfied by any matching image file from anywhere # in the same directory it came from. The resources under the *first* # directory argument (only) are visible everywhere. # # (3) A reference with embedded {}s in a macro will have the macro's # formal args substituted in at WML evaluation time. Instead, this # tool treats each {} as a .* wildcard and considers the reference to # match *every* resource filename that matches that pattern. Under # appropriate circumstances this might report a resource filename # statically matching the pattern as having been referenced even # though none of the actual macro calls would actually generate it. # # Problems (1) and (2) imply that this tool might conceivably report # that a reference has been satisfied when under actual # WML-interpreter rules it has not. # # The reporting format is compatible with GNU Emacs compile mode. # # For debugging purposes, an in-line comment of the form # # # wmlscope: warnlevel NNN # # sets the warning level. import sys, os, time, re, getopt, md5 from wesnoth.wmltools import * def interpret(lines, css): "Interpret the ! convention for .cfg comments." inlisting = False outstr = '
' % css for line in lines: line = line.strip() if not inlisting and not line: outstr += "
" continue if not inlisting and line[0] == '!': outstr += "
\n" inlisting = True bracketdepth = curlydepth = 0 line = line.replace("<", "<").replace(">", ">").replace("&", "&") if inlisting: outstr += line[1:] + "\n" else: outstr += line + "\n" if inlisting: if line and line[0] != '!': outstr += "\n
" inlisting = False if not inlisting: outstr += "
\n" else: outstr += "\n" outstr = outstr.replace("", "") outstr = outstr.replace("\n\n", "\n") return outstr class CrossRefLister(CrossRef): "Cross-reference generator with reporting functions" def xrefdump(self, pred=None): "Report resolved macro references." sorted = self.xref.keys() sorted.sort() for name in sorted: for defn in self.xref[name]: if pred and not pred(name, defn): continue if defn.undef: type = "local" else: type = "global" nrefs = len(defn.references) if nrefs == 0: print "%s: %s macro %s is unused" % (defn, type, name) else: print "%s: %s macro %s is used in %d files:" % (defn, type, name, nrefs) defn.dump_references() sorted = self.fileref.keys() sorted.sort() for name in sorted: defloc = self.fileref[name] if pred and not pred(name, defloc): continue nrefs = len(defloc.references) if nrefs == 0: print "Resource %s is unused" % defloc else: print "Resource %s is used in %d files:" % (defloc, nrefs) defloc.dump_references() def unresdump(self): "Report unresolved references and arity mismatches." # First the unresolved references if len(self.unresolved) == 0 and len(self.missing) == 0: print "# No unresolved references" else: #print self.fileref.keys() print "# Unresolved references:" for (name, reference) in self.unresolved + self.missing: print "%s -> %s" % (reference, name) mismatched = [] sorted = self.xref.keys() sorted.sort() for name in sorted: for defn in self.xref[name]: m = defn.mismatches() if m.references: mismatched.append((name, m)) # Then the type mismatches if mismatched: print "# Mismatched references:" for (n, m) in mismatched: print "%s: macro %s(%s) has mismatches:" % (m, n, ", ".join(map(lambda x: "%s=%s" % (x, formaltype(x)), m.args))) for (file, refs) in m.references.items(): for (ln, args) in refs: try: print '"%s", line %d: %s(%s) with signature (%s)' % (file, ln, n, ", ".join(args), ", ".join(map(lambda f, a: "%s=%s" % (f, actualtype(a)), m.args, args))) except AttributeError: print '"%s", line %d: internal error in reporter' % (file, ln) def typelist(self, branch): "Dump actual and formal aruments for macros in specified file" already_seen = [] sorted = self.xref.keys() sorted.sort() for name in sorted: for defn in self.xref[name]: for (filename, refs) in defn.references.items(): if filename.endswith(branch): if name not in already_seen: already_seen.append(name) print "%s: macro %s(%s):" % (defn, name, ", ".join(map(lambda x: "%s=%s" % (x, formaltype(x)), defn.args))) for (ln, args) in refs: print '"%s", line %d: %s(%s) with signature (%s)' % (filename, ln, name, ", ".join(args), ", ".join(map(lambda f, a: "%s=%s" % (f, actualtype(a)), defn.args, args))) def deflist(self, pred=None): "List all resource definitions." sorted = self.xref.keys() sorted.sort() for name in sorted: for defn in self.xref[name]: if not pred or pred(name, defn): print name sorted = self.fileref.keys() sorted.sort() for name in sorted: defloc = self.fileref[name] if not pred or pred(name, defloc): print name def unchecked(self, fp): "List all macro definitions with untyped formals." unchecked = [] defcount = 0 callcount = 0 unresolvedcount = 0 for name in self.xref.keys(): for defn in self.xref[name]: defcount += 1 callcount += len(defn.references) if None in map(formaltype, defn.args): unchecked.append((name, defn)) unresolvedcount += len(defn.references) if unchecked: print "# %d of %d (%d%%) macro definitions and %d of %d calls (%d%%) have untyped formals:" \ % (len(unchecked), defcount, int((100 * len(unchecked)) / defcount), unresolvedcount, callcount, int((100 * unresolvedcount) / callcount)) unchecked.sort(lambda a, b: cmp(a[1], b[1])) for (name, defn) in unchecked: print "%s: %s(%s)" % (defn, name, ", ".join(defn.args)) def extracthelp(self, pref, fp): "Deliver all macro help comments in HTML form." # Bug: finds only the first definition of each macro in scope. doclist = self.xref.keys() doclist = filter(lambda x: self.xref[x][0].docstring.count("\n") > 1, doclist) doclist.sort(lambda x, y: cmp(self.xref[x][0], self.xref[y][0])) outstr = "" filename = None counted = 0 for name in doclist: entry = self.xref[name][0] if entry.filename != filename: if counted: outstr += "\n" counted += 1 filename = entry.filename if filename.startswith(pref): displayname = filename[len(pref):] else: displayname = filename outstr += "