wmlscope: detect and analyze optional macro arguments
This commit is contained in:
parent
cba53cd471
commit
ec41359368
4 changed files with 111 additions and 40 deletions
|
@ -91,6 +91,7 @@
|
|||
* Added tool `tmx_trackplacer`, a file converter for editing map tracks with Tiled (PR #4464)
|
||||
* Added tool trackviewer, which has the animation-preview functions of trackplacer (PR #4574)
|
||||
* Removed the python2 trackplacer tool (issue #4365)
|
||||
* Made wmlscope recognize and analyze optional macro arguments
|
||||
|
||||
## Version 1.15.2
|
||||
### AI:
|
||||
|
|
|
@ -287,9 +287,33 @@ def isresource(filename):
|
|||
return ext and ext[1:] in resource_extensions
|
||||
|
||||
def parse_macroref(start, line):
|
||||
def handle_argument():
|
||||
nonlocal opt_arg
|
||||
nonlocal arg
|
||||
nonlocal optional_args
|
||||
nonlocal args
|
||||
|
||||
arg = arg.strip()
|
||||
# is this an optional argument?
|
||||
# argument names are usually made of uppercase letters, numbers and underscores
|
||||
# if they're optional, they're followed by an equal sign
|
||||
# stop matching on the first one, because the argument value might contain one too
|
||||
if re.match(r"^([A-Z0-9_]+?)=", arg):
|
||||
opt_arg, arg = arg.split("=", 1)
|
||||
if arg.startswith('"') and arg.endswith('"'):
|
||||
arg = arg[1:-1].strip()
|
||||
if opt_arg:
|
||||
optional_args[opt_arg] = arg
|
||||
opt_arg = ""
|
||||
else:
|
||||
args.append(arg)
|
||||
arg = ""
|
||||
|
||||
brackdepth = parendepth = 0
|
||||
instring = False
|
||||
optional_args = {}
|
||||
args = []
|
||||
opt_arg = ""
|
||||
arg = ""
|
||||
for i in range(start, len(line)):
|
||||
if instring:
|
||||
|
@ -307,11 +331,7 @@ def parse_macroref(start, line):
|
|||
brackdepth -= 1
|
||||
if brackdepth == 0:
|
||||
if not line[i-1].isspace():
|
||||
arg = arg.strip()
|
||||
if arg.startswith('"') and arg.endswith('"'):
|
||||
arg = arg[1:-1].strip()
|
||||
args.append(arg)
|
||||
arg = ""
|
||||
handle_argument()
|
||||
break
|
||||
else:
|
||||
arg += line[i]
|
||||
|
@ -323,14 +343,10 @@ def parse_macroref(start, line):
|
|||
line[i].isspace() and \
|
||||
brackdepth == 1 and \
|
||||
parendepth == 0:
|
||||
arg = arg.strip()
|
||||
if arg.startswith('"') and arg.endswith('"'):
|
||||
arg = arg[1:-1].strip()
|
||||
args.append(arg)
|
||||
arg = ""
|
||||
handle_argument()
|
||||
elif not line[i].isspace() or parendepth > 0:
|
||||
arg += line[i]
|
||||
return (args, brackdepth, parendepth)
|
||||
return (args, optional_args, brackdepth, parendepth)
|
||||
|
||||
def formaltype(f):
|
||||
# Deduce the expected type of the formal
|
||||
|
@ -419,10 +435,18 @@ def actualtype(a):
|
|||
atype = "string"
|
||||
return atype
|
||||
|
||||
def argmatch(formals, actuals):
|
||||
def argmatch(formals, optional_formals, actuals, optional_actuals):
|
||||
if optional_formals:
|
||||
for key in optional_actuals.keys():
|
||||
if key not in optional_formals:
|
||||
return False
|
||||
if len(formals) != len(actuals):
|
||||
return False
|
||||
for (f, a) in zip(formals, actuals):
|
||||
opt_formals, opt_actuals = [], []
|
||||
for key, value in optional_actuals.items():
|
||||
opt_formals.append(key)
|
||||
opt_actuals.append(value)
|
||||
for (f, a) in zip(formals + opt_formals, actuals + opt_actuals):
|
||||
# Here's the compatibility logic. First, we catch the situations
|
||||
# in which a more restricted actual type matches a more general
|
||||
# formal one. Then we have a fallback rule checking for type
|
||||
|
@ -465,17 +489,18 @@ def argmatch(formals, actuals):
|
|||
@total_ordering
|
||||
class Reference:
|
||||
"Describes a location by file and line."
|
||||
def __init__(self, namespace, filename, lineno=None, docstring=None, args=None):
|
||||
def __init__(self, namespace, filename, lineno=None, docstring=None, args=None, optional_args=None):
|
||||
self.namespace = namespace
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.docstring = docstring
|
||||
self.args = args
|
||||
self.optional_args = optional_args
|
||||
self.references = collections.defaultdict(list)
|
||||
self.undef = None
|
||||
|
||||
def append(self, fn, n, a=None):
|
||||
self.references[fn].append((n, a))
|
||||
def append(self, fn, n, args=None, optional_args=None):
|
||||
self.references[fn].append((n, args, optional_args))
|
||||
|
||||
def dump_references(self):
|
||||
"Dump all known references to this definition."
|
||||
|
@ -495,10 +520,10 @@ class Reference:
|
|||
return self.filename > other.filename
|
||||
|
||||
def mismatches(self):
|
||||
copy = Reference(self.namespace, self.filename, self.lineno, self.docstring, self.args)
|
||||
copy = Reference(self.namespace, self.filename, self.lineno, self.docstring, self.args, self.optional_args)
|
||||
copy.undef = self.undef
|
||||
for filename in self.references:
|
||||
mis = [(ln,a) for (ln,a) in self.references[filename] if a is not None and not argmatch(self.args, a)]
|
||||
mis = [(ln,a,oa) for (ln,a,oa) in self.references[filename] if a is not None and not argmatch(self.args, self.optional_args, a, oa)]
|
||||
if mis:
|
||||
copy.references[filename] = mis
|
||||
return copy
|
||||
|
@ -617,7 +642,7 @@ class CrossRef:
|
|||
% (filename, n+1), file=sys.stderr)
|
||||
else:
|
||||
name = tokens[1]
|
||||
here = Reference(namespace, filename, n+1, line, args=tokens[2:])
|
||||
here = Reference(namespace, filename, n+1, line, args=tokens[2:], optional_args=[])
|
||||
here.hash = hashlib.md5()
|
||||
here.docstring = line.lstrip()[8:] # Strip off #define_
|
||||
state = "macro_header"
|
||||
|
@ -641,13 +666,19 @@ class CrossRef:
|
|||
self.xref[name] = []
|
||||
self.xref[name].append(here)
|
||||
state = "outside"
|
||||
elif state == "macro_header" and line.strip() and line.strip()[0] != "#":
|
||||
state = "macro_body"
|
||||
elif state == "macro_header" and line.strip():
|
||||
if line.strip().startswith("#arg"):
|
||||
state = "macro_optional_argument"
|
||||
here.optional_args.append(line.strip().split()[1])
|
||||
elif line.strip()[0] != "#":
|
||||
state = "macro_body"
|
||||
elif state == "macro_optional_argument" and "#endarg" in line:
|
||||
state = "macro_header"
|
||||
if state == "macro_header":
|
||||
# Ignore macro header commends that are pragmas
|
||||
if "wmlscope" not in line and "wmllint:" not in line:
|
||||
here.docstring += line.lstrip()[1:]
|
||||
if state in ("macro_header", "macro_body"):
|
||||
if state in ("macro_header", "macro_optional_argument", "macro_body"):
|
||||
here.hash.update(line.encode("utf8"))
|
||||
elif line.strip().startswith("#undef"):
|
||||
tokens = line.split()
|
||||
|
@ -712,6 +743,7 @@ class CrossRef:
|
|||
self.unresolved = []
|
||||
self.missing = []
|
||||
formals = []
|
||||
optional_formals = []
|
||||
state = "outside"
|
||||
if self.warnlevel >=2 or progress:
|
||||
print("*** Beginning reference-gathering pass...")
|
||||
|
@ -724,11 +756,17 @@ class CrossRef:
|
|||
have_icon = False
|
||||
beneath = 0
|
||||
ignoreflag = False
|
||||
in_macro_definition = False
|
||||
for (n, line) in enumerate(rfp):
|
||||
if line.strip().startswith("#define"):
|
||||
formals = line.strip().split()[2:]
|
||||
in_macro_definition = True
|
||||
elif line.startswith("#enddef"):
|
||||
formals = []
|
||||
optional_formals = []
|
||||
in_macro_definition = False
|
||||
elif in_macro_definition and line.startswith("#arg"):
|
||||
optional_formals.append(line.strip().split()[1])
|
||||
comment = ""
|
||||
if '#' in line:
|
||||
if "# wmlscope: start ignoring" in line:
|
||||
|
@ -760,15 +798,16 @@ class CrossRef:
|
|||
if self.warnlevel >=2:
|
||||
print('"%s", line %d: seeking definition of %s' \
|
||||
% (fn, n+1, name))
|
||||
if name in formals:
|
||||
if name in formals or name in optional_formals:
|
||||
continue
|
||||
elif name in self.xref:
|
||||
# Count the number of actual arguments.
|
||||
# Set args to None if the call doesn't
|
||||
# close on this line
|
||||
(args, brackdepth, parendepth) = parse_macroref(match.start(0), line)
|
||||
(args, optional_args, brackdepth, parendepth) = parse_macroref(match.start(0), line)
|
||||
if brackdepth > 0 or parendepth > 0:
|
||||
args = None
|
||||
optional_args = None
|
||||
else:
|
||||
args.pop(0)
|
||||
#if args:
|
||||
|
@ -777,7 +816,7 @@ class CrossRef:
|
|||
# Figure out which macros might resolve this
|
||||
for defn in self.xref[name]:
|
||||
if self.visible_from(defn, fn, n+1):
|
||||
defn.append(fn, n+1, args)
|
||||
defn.append(fn, n+1, args, optional_args)
|
||||
candidates.append(str(defn))
|
||||
if len(candidates) > 1:
|
||||
print("%s: more than one definition of %s is visible here (%s)." % (Reference(ns, fn, n), name, "; ".join(candidates)))
|
||||
|
|
|
@ -2048,12 +2048,12 @@ def global_sanity_check(filename, lines):
|
|||
# this here rather than hack_syntax so the character can be
|
||||
# recognized.
|
||||
if macname == 'LOYAL_UNIT':
|
||||
(args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
(args, optional_args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
if len(args) == 7:
|
||||
lines[i] = lines[i].replace('{LOYAL_UNIT', '{NAMED_LOYAL_UNIT', 1)
|
||||
# Auto-recognize the people in the {NAMED_*UNIT} macros.
|
||||
if re.match(r'NAMED_[A-Z_]*UNIT$', macname):
|
||||
(args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
(args, optional_args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
if len(args) >= 7 and \
|
||||
re.match(r'([0-9]+|[^\s]*\$[^\s]*side[^\s]*|{[^\s]*SIDE[^\s]*})$', args[1]) and \
|
||||
re.match(r'([0-9]+|[^\s]*\$[^\s]*x[^\s]*|{[^\s]*X[^\s]*})$', args[3]) and \
|
||||
|
@ -2061,20 +2061,20 @@ def global_sanity_check(filename, lines):
|
|||
len(args[5]) > 0:
|
||||
present.append(args[5])
|
||||
elif macname == 'RECALL':
|
||||
(args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
(args, optional_args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
if len(args) == 2 and brack == 0:
|
||||
present.append(args[1])
|
||||
elif macname == 'RECALL_XY':
|
||||
(args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
(args, optional_args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
if len(args) == 4:
|
||||
present.append(args[1])
|
||||
elif macname == 'CLEAR_VARIABLE':
|
||||
(args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
(args, optional_args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
for arg in [x.lstrip() for x in args[1].split(',')]:
|
||||
if arg in storedids:
|
||||
del storedids[arg]
|
||||
elif macname in whomacros:
|
||||
(args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
(args, optional_args, brack, paren) = parse_macroref(0, leadmac.string)
|
||||
present.append(args[whomacros[macname]])
|
||||
m = re.search("# *wmllint: recognize +(.*)", lines[i])
|
||||
if m:
|
||||
|
|
|
@ -175,10 +175,23 @@ class CrossRefLister(CrossRef):
|
|||
if mismatched:
|
||||
print("# Mismatched references:")
|
||||
for (n, m) in mismatched:
|
||||
print("%s: macro %s(%s) has mismatches:" % (m, n, ", ".join(["{}={}".format(x, formaltype(x)) for x in m.args])))
|
||||
print("%s: macro %s(%s%s%s) has mismatches:" % (m,
|
||||
n,
|
||||
", ".join(["{}={}".format(x, formaltype(x)) for x in m.args]),
|
||||
" " if m.optional_args else "",
|
||||
", ".join(["{}=optional {}".format(x, formaltype(x)) for x in m.optional_args])))
|
||||
for (file, refs) in m.references.items():
|
||||
for (ln, args) in refs:
|
||||
print('"%s", line %d: %s(%s) with signature (%s)' % (file, ln, n, ", ".join(args), ", ".join(["{}={}".format(f, actualtype(a)) for f,a in zip(m.args, args)])))
|
||||
for (ln, args, optional_args) in refs:
|
||||
print('"%s", line %d: %s(%s%s%s) with signature (%s%s%s)' %
|
||||
(file,
|
||||
ln,
|
||||
n,
|
||||
", ".join(args),
|
||||
", " if optional_args else "",
|
||||
", ".join(optional_args.values()),
|
||||
", ".join(["{}={}".format(f, actualtype(a)) for f,a in zip(m.args, args)]),
|
||||
", " if optional_args else "",
|
||||
", ".join(["{}=optional {}".format(f, actualtype(oa)) for f,oa in optional_args.items()])))
|
||||
|
||||
def incorrectlysized(self):
|
||||
"Report incorrectly sized images that cannot be safely used for their intended purpose"
|
||||
|
@ -258,15 +271,33 @@ class CrossRefLister(CrossRef):
|
|||
if filename.endswith(branch):
|
||||
if name not in already_seen:
|
||||
already_seen.append(name)
|
||||
print("%s: macro %s(%s):" % (defn, name, ", ".join(["{}={}".format(x, formaltype(x)) for x in defn.args])))
|
||||
for (ln, args) in refs:
|
||||
print('"%s", line %d: %s(%s) with signature (%s)' % (filename, ln, name, ", ".join(args), ", ".join(["{}={}".format(f, actualtype(a)) for f,a in zip(defn.args, args)])))
|
||||
print("%s: macro %s(%s%s%s):" %
|
||||
(defn,
|
||||
name,
|
||||
", ".join(["{}={}".format(x, formaltype(x)) for x in defn.args]),
|
||||
", " if defn.optional_args else "",
|
||||
", ".join(["{}=optional {}".format(x, formaltype(x)) for x in defn.optional_args])))
|
||||
for (ln, args, optional_args) in refs:
|
||||
print('"%s", line %d: %s(%s%s%s) with signature (%s%s%s)' %
|
||||
(filename,
|
||||
ln,
|
||||
name,
|
||||
", ".join(args),
|
||||
", " if optional_args else "",
|
||||
", ".join(optional_args.values()),
|
||||
", ".join(["{}={}".format(f, actualtype(a)) for f,a in zip(defn.args, args)]),
|
||||
", " if optional_args else "",
|
||||
", ".join(["{}={}".format(f, actualtype(oa)) for f,oa in optional_args.items()])))
|
||||
|
||||
def deflist(self, pred=None):
|
||||
"List all resource definitions."
|
||||
for name in sorted(self.xref.keys()):
|
||||
for defn in self.xref[name]:
|
||||
if not pred or pred(name, defn):
|
||||
print("macro", name, " ".join(["{}={}".format(x, formaltype(x)) for x in defn.args]))
|
||||
print("macro",
|
||||
name,
|
||||
" ".join(["{}={}".format(x, formaltype(x)) for x in defn.args]),
|
||||
" ".join(["{}=optional {}".format(x, formaltype(x)) for x in defn.optional_args]))
|
||||
for name in sorted(self.fileref.keys()):
|
||||
defloc = self.fileref[name]
|
||||
if not pred or pred(name, defloc):
|
||||
|
@ -291,7 +322,7 @@ class CrossRefLister(CrossRef):
|
|||
unchecked.append((name, defn))
|
||||
unresolvedcount += len(defn.references)
|
||||
if unchecked:
|
||||
print("# %d of %d (%.02f%%) macro definitions and %d of %d calls (%.02f%%) have untyped formals:" \
|
||||
print("# %d of %d (%.02f%%) macro definitions and %d of %d calls (%.02f%%) have untyped required formals:" \
|
||||
% (len(unchecked),
|
||||
defcount,
|
||||
((100 * len(unchecked)) / defcount),
|
||||
|
@ -301,7 +332,7 @@ class CrossRefLister(CrossRef):
|
|||
# sort by checking the 2nd element in the tuple
|
||||
unchecked.sort(key=lambda element: element[1])
|
||||
for (name, defn) in unchecked:
|
||||
print("%s: %s(%s)" % (defn, name, ", ".join(defn.args)))
|
||||
print("%s: %s(%s)" % (defn, name, ", ".join(defn.args),))
|
||||
|
||||
def extracthelp(self, pref, fp):
|
||||
"Deliver all macro help comments in HTML form."
|
||||
|
|
Loading…
Add table
Reference in a new issue