CampBuddy/Camp.Buddy v2.2.1/Camp_Buddy-2.2.1-pc/renpy/lint.py
2025-03-03 23:00:33 +01:00

876 lines
21 KiB
Python

# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
import renpy.display
import renpy.text
import codecs
import time
import re
import sys
import collections
import textwrap
import __builtin__
python_builtins = set(dir(__builtin__))
renpy_builtins = set()
image_prefixes = None
# Things to check in lint.
#
# Image files exist, and are of the right case.
# Jump/Call targets defined.
# Say whos can evaluate.
# Call followed by say.
# Show/Scene valid.
# At valid.
# With valid.
# Hide maybe valid.
# Expressions can compile.
# The node the report will be about:
report_node = None
# Reports a message to the user.
def report(msg, *args):
if report_node:
out = u"%s:%d " % (renpy.parser.unicode_filename(report_node.filename), report_node.linenumber)
else:
out = ""
out += msg % args
print()
print(out.encode('utf-8'))
added = { }
# Reports additional information about a message, the first time it
# occurs.
def add(msg, *args):
if not msg in added:
added[msg] = True
msg = unicode(msg) % args
print(msg.encode('utf-8'))
# Tries to evaluate an expression, announcing an error if it fails.
def try_eval(where, expr, additional=None):
"""
:doc: lint
Tries to evaluate an expression, and writes an error to lint.txt if
it fails.
`where`
A string giving the location the expression is found. Used to
generate an error message of the form "Could not evaluate `expr`
in `where`."
`expr`
The expression to try evaluating.
`additional`
If given, an additional line of information that is addded to the
error message.
"""
# Make sure the expression compiles.
try_compile(where, expr)
# Simply look up the first component of the python expression, and
# see if it exists in the store.
m = re.match(r'\s*([a-zA-Z_]\w*)', expr)
if not m:
return
if hasattr(renpy.store, m.group(1)):
return
if m.group(1) in __builtins__:
return
report("Could not evaluate '%s', in %s.", expr, where)
if additional:
add(additional)
# Returns True of the expression can be compiled as python, False
# otherwise.
def try_compile(where, expr, additional=None):
"""
:doc: lint
Tries to compile an expression, and writes an error to lint.txt if
it fails.
`where`
A string giving the location the expression is found. Used to
generate an error message of the form "Could not evaluate `expr`
in `where`."
`expr`
The expression to try compiling.
`additional`
If given, an additional line of information that is addded to the
error message.
"""
try:
renpy.python.py_compile_eval_bytecode(expr)
except:
report("'%s' could not be compiled as a python expression, %s.", expr, where)
if additional:
add(additional)
# The sets of names + attributes that we know are valid.
imprecise_cache = set()
def image_exists_imprecise(name):
"""
Returns true if the image is a plausible image that can be used in a show
statement. This returns true if at least one image exists with the same
tag and containing all of the attributes (and none of the removed attributes).
"""
if name in imprecise_cache:
return True
nametag = name[0]
required = set()
banned = set()
for i in name[1:]:
if i[0] == "-":
banned.add(i[1:])
else:
required.add(i)
for im, d in renpy.display.image.images.items():
if im[0] != nametag:
continue
attrs = set(im[1:])
if [ i for i in banned if i in attrs ]:
continue
li = getattr(d, "_list_attributes", None)
if li is not None:
attrs = attrs | set(li(im[0], required))
if [ i for i in required if i not in attrs ]:
continue
imprecise_cache.add(name)
return True
return False
precise_cache = set()
def image_exists_precise(name):
"""
Returns true if an image exists with the same tag and attributes as
`name`. (The attributes are allowed to occur in any order.)
"""
if name in precise_cache:
return True
nametag = name[0]
required = set()
banned = set()
for i in name[1:]:
if i[0] == "-":
banned.add(i[1:])
else:
required.add(i)
for im, d in renpy.display.image.images.items():
if im[0] != nametag:
continue
attrs = set(im[1:])
if attrs - required:
continue
rest = required - attrs
if rest:
try:
da = renpy.display.core.DisplayableArguments()
da.name=( im[0], ) + tuple(i for i in name[1:] if i in attrs)
da.args=tuple(i for i in name[1:] if i in rest)
da.lint = True
d._duplicate(da)
except:
continue
precise_cache.add(name)
return True
return False
# This reports an error if we're sure that the image with the given name
# does not exist.
def image_exists(name, expression, tag, precise=True):
"""
Checks a scene or show statement for image existence.
"""
# Add the tag to the set of known tags.
tag = tag or name[0]
image_prefixes[tag] = True
if expression:
return
if not precise:
if image_exists_imprecise(name):
return
# If we're not precise, then we have to start looking for images
# that we can possibly match.
if image_exists_precise(name):
return
report("'%s' is not an image.", " ".join(name))
# Only check each file once.
check_file_cache = { }
def check_file(what, fn):
present = check_file_cache.get(fn, None)
if present is True:
return
if present is False:
report("%s uses file '%s', which is not loadable.", what.capitalize(), fn)
return
if not renpy.loader.loadable(fn):
report("%s uses file '%s', which is not loadable.", what.capitalize(), fn)
check_file_cache[fn] = False
return
check_file_cache[fn] = True
def check_displayable(what, d):
def predict_image(img):
files.extend(img.predict_files())
renpy.display.predict.image = predict_image
files = [ ]
try:
if isinstance(d, renpy.display.core.Displayable):
d.visit_all(lambda a: a.predict_one())
except:
pass
for fn in files:
check_file(what, fn)
# Lints ast.Image nodes.
def check_image(node):
name = " ".join(node.imgname)
check_displayable('image %s' % name, renpy.display.image.images[node.imgname])
def imspec(t):
if len(t) == 3:
return t[0], None, None, t[1], t[2], 0
if len(t) == 6:
return t[0], t[1], t[2], t[3], t[4], t[5], None
else:
return t
# Lints ast.Show and ast.Scene nodes.
def check_show(node, precise):
# A Scene may have an empty imspec.
if not node.imspec:
return
name, expression, tag, at_list, layer, _zorder, _behind = imspec(node.imspec)
layer = renpy.exports.default_layer(layer, tag or name)
if layer not in renpy.config.layers and layer not in renpy.config.top_layers:
report("Uses layer '%s', which is not in config.layers.", layer)
image_exists(name, expression, tag, precise=precise)
for i in at_list:
try_eval("the at list of a scene or show statment", i, "Perhaps you forgot to define or misspelled a transform.")
def precheck_show(node):
# A Scene may have an empty imspec.
if not node.imspec:
return
tag = imspec(node.imspec)[2]
image_prefixes[tag] = True
# Lints ast.Hide.
def check_hide(node):
name, _expression, tag, _at_list, layer, _zorder, _behind = imspec(node.imspec)
tag = tag or name[0]
layer = renpy.exports.default_layer(layer, tag)
if layer not in renpy.config.layers and layer not in renpy.config.top_layers:
report("Uses layer '%s', which is not in config.layers.", layer)
if tag not in image_prefixes:
report("The image tag '%s' is not the prefix of a declared image, nor was it used in a show statement before this hide statement.", tag)
def check_with(node):
try_eval("a with statement or clause", node.expr, "Perhaps you forgot to declare, or misspelled, a transition?")
def check_user(node):
def error(msg):
report("%s", msg)
renpy.exports.push_error_handler(error)
try:
node.call("lint")
finally:
renpy.exports.pop_error_handler()
try:
node.get_next()
except:
report("Didn't properly report what the next statement should be.")
def text_checks(s):
msg = renpy.text.extras.check_text_tags(s)
if msg:
report("%s (in %s)", msg, repr(s)[1:])
if "%" in s and renpy.config.old_substitutions:
state = 0
pos = 0
fmt = ""
while pos < len(s):
c = s[pos]
pos += 1
# Not in a format.
if state == 0:
if c == "%":
state = 1
fmt = "%"
# In a format.
elif state == 1:
fmt += c
if c == "(":
state = 2
elif c in "#0123456780- +hlL":
state = 1
elif c in "diouxXeEfFgGcrs%":
state = 0
else:
report("Unknown string format code '%s' (in %s)", fmt, repr(s)[1:])
state = 0
# In a mapping key.
elif state == 2:
fmt += c
if c == ")":
state = 1
if state != 0:
report("Unterminated string format code '%s' (in %s)", fmt, repr(s)[1:])
def check_say(node):
if node.who:
try:
char = renpy.ast.eval_who(node.who)
except:
report("Could not evaluate '%s' in the who part of a say statement.", node.who)
add("Perhaps you forgot to define a character?")
char = None
if node.with_:
try_eval("the with clause of a say statement", node.with_, "Perhaps you forgot to declare, or misspelled, a transition?")
text_checks(node.what)
if not node.who_fast:
return
# Code to check image attributes. (If we're lucky.)
if node.who is None:
return
if not isinstance(char, renpy.character.ADVCharacter):
return
if node.attributes is None:
return
if char.image_tag is None:
return
name = (char.image_tag,) + node.attributes
if image_exists_imprecise(name):
return
if image_exists_imprecise(('side', ) + name):
return
report("Could not find image (%s) corresponding to attributes on say statement.", " ".join(name))
def check_menu(node):
if node.with_:
try_eval("the with clause of a menu statement", node.with_, "Perhaps you forgot to declare, or misspelled, a transition?")
if not [ (l, c, b) for l, c, b in node.items if b ]:
report("The menu does not contain any selectable choices.")
for l, c, b in node.items:
if c:
try_compile("in the if clause of a menuitem", c)
text_checks(l)
def check_jump(node):
if node.expression:
return
if not renpy.game.script.has_label(node.target):
report("The jump is to nonexistent label '%s'.", node.target)
def check_call(node):
if node.expression:
return
if not renpy.game.script.has_label(node.label):
report("The call is to nonexistent label '%s'.", node.label)
def check_while(node):
try_compile("in the condition of the while statement", node.condition)
def check_if(node):
for condition, _block in node.entries:
try_compile("in a condition of the if statement", condition)
def check_define(node, kind):
if node.store != 'store':
return
if node.varname in renpy.config.lint_ignore_replaces:
return
if node.varname in python_builtins:
report("'%s %s' replaces a python built-in name, which may cause problems.", kind, node.varname)
if node.varname in renpy_builtins:
report("'%s %s' replaces a Ren'Py built-in name, which may cause problems.", kind, node.varname)
def check_style_property_displayable(name, property, d):
if not d._duplicatable:
check_displayable(
"{}, property {}".format(name, property),
d)
return
renpy.style.init_inspect()
def sort_short(l):
l = list(l)
l.sort(key=lambda a: len(a))
return l
alts = sort_short(renpy.style.prefix_alts)
for p in sort_short(renpy.style.affects.get(property, [ ])):
for prefix in alts:
rest = p[len(prefix):]
if rest in renpy.style.all_properties:
args = d._args.copy(prefix=prefix)
dd = d._duplicate(args)
dd._unique()
check_displayable(
"{}, property {}".format(name, prefix + property),
dd)
break
# print property, p
def check_style(name, s):
for p in s.properties:
for k, v in p.iteritems():
# Treat font specially.
if k.endswith("font"):
if isinstance(v, renpy.text.font.FontGroup):
for f in set(v.map.values()):
check_file(name, f)
else:
check_file(name, v)
if isinstance(v, renpy.display.core.Displayable):
check_style_property_displayable(name, k, v)
def check_label(node):
def add_arg(n):
if n is None:
return
if not hasattr(renpy.store, n):
setattr(renpy.store, n, None)
pi = node.parameters
if pi is not None:
for i in pi.positional:
add_arg(i)
add_arg(pi.extrapos)
add_arg(pi.extrakw)
def check_screen(node):
if (node.screen.parameters is None) and renpy.config.lint_screens_without_parameters:
report("The screen %s has not been given a parameter list.", node.screen.name)
add("This can be fixed by writing 'screen %s():' instead.", node.screen.name)
def check_styles():
for full_name, s in renpy.style.styles.iteritems(): # @UndefinedVariable
name = "style." + full_name[0]
for i in full_name[1:]:
name += "[{!r}]".format(i)
check_style("Style " + name, s)
def humanize(n):
s = str(n)
rv = []
for i, c in enumerate(reversed(s)):
if i and not (i % 3):
rv.insert(0, ',')
rv.insert(0, c)
return ''.join(rv)
def check_filename_encodings():
"""
Checks files to ensure that they are displayable in unicode.
"""
for _dirname, filename in renpy.loader.listdirfiles():
try:
filename.encode("ascii")
continue
except:
pass
report("%s contains non-ASCII characters in its filename.", filename)
add("(ZIP file distributions can only reliably include ASCII filenames.)")
class Count(object):
"""
Stores information about the word count.
"""
def __init__(self):
# The number of blocks of text.
self.blocks = 0
# The number of whitespace-separated words.
self.words = 0
# The number of characters.
self.characters = 0
def add(self, s):
self.blocks += 1
self.words += len(s.split())
self.characters += len(s)
def common(n):
"""
Returns true if the node is in the common directory.
"""
filename = n.filename.replace("\\", "/")
if filename.startswith("common/") or filename.startswith("renpy/common/"):
return True
else:
return False
def lint():
"""
The master lint function, that's responsible for staging all of the
other checks.
"""
ap = renpy.arguments.ArgumentParser(description="Checks the script for errors and prints script statistics.", require_command=False)
ap.add_argument("filename", nargs='?', action="store", help="The file to write to.")
args = ap.parse_args()
if args.filename:
f = open(args.filename, "w")
sys.stdout = f
renpy.game.lint = True
print(codecs.BOM_UTF8)
print(unicode(renpy.version + " lint report, generated at: " + time.ctime()).encode("utf-8"))
# This supports check_hide.
global image_prefixes
image_prefixes = { }
for k in renpy.display.image.images:
image_prefixes[k[0]] = True
# Iterate through every statement in the program, processing
# them. We sort them in filename, linenumber order.
all_stmts = [ (i.filename, i.linenumber, i) for i in renpy.game.script.all_stmts ]
all_stmts.sort()
# The current count.
counts = collections.defaultdict(Count)
# The current language.
language = None
menu_count = 0
screen_count = 0
image_count = 0
global report_node
for _fn, _ln, node in all_stmts:
if isinstance(node, (renpy.ast.Show, renpy.ast.Scene)):
precheck_show(node)
for _fn, _ln, node in all_stmts:
if common(node):
continue
report_node = node
if isinstance(node, renpy.ast.Image):
image_count += 1
check_image(node)
elif isinstance(node, renpy.ast.Show):
check_show(node, False)
elif isinstance(node, renpy.ast.Scene):
check_show(node, True)
elif isinstance(node, renpy.ast.Hide):
check_hide(node)
elif isinstance(node, renpy.ast.With):
check_with(node)
elif isinstance(node, renpy.ast.Say):
check_say(node)
counts[language].add(node.what)
elif isinstance(node, renpy.ast.Menu):
check_menu(node)
menu_count += 1
elif isinstance(node, renpy.ast.Jump):
check_jump(node)
elif isinstance(node, renpy.ast.Call):
check_call(node)
elif isinstance(node, renpy.ast.While):
check_while(node)
elif isinstance(node, renpy.ast.If):
check_if(node)
elif isinstance(node, renpy.ast.UserStatement):
check_user(node)
elif isinstance(node, renpy.ast.Label):
check_label(node)
elif isinstance(node, renpy.ast.Translate):
language = node.language
elif isinstance(node, renpy.ast.EndTranslate):
language = None
elif isinstance(node, renpy.ast.Screen):
screen_count += 1
check_screen(node)
elif isinstance(node, renpy.ast.Define):
check_define(node, "define")
elif isinstance(node, renpy.ast.Default):
check_define(node, "default")
report_node = None
check_styles()
check_filename_encodings()
for f in renpy.config.lint_hooks:
f()
lines = [ ]
def report_language(language):
count = counts[language]
if count.blocks <= 0:
return
if language is None:
s = "The game"
else:
s = "The {0} translation".format(language)
s += """ contains {0} dialogue blocks, containing {1} words
and {2} characters, for an average of {3:.1f} words and {4:.0f}
characters per block. """.format(
humanize(count.blocks),
humanize(count.words),
humanize(count.characters),
1.0 * count.words / count.blocks,
1.0 * count.characters / count.blocks)
lines.append(s)
print()
print()
print("Statistics:")
print()
languages = list(counts)
languages.sort()
for i in languages:
report_language(i)
lines.append("The game contains {0} menus, {1} images, and {2} screens.".format(
humanize(menu_count), humanize(image_count), humanize(screen_count)))
for l in lines:
for ll in textwrap.wrap(l, 78):
print(ll.encode("utf-8"))
print()
for i in renpy.config.lint_stats_callbacks:
i()
print()
if renpy.config.developer and (renpy.config.original_developer != "auto"):
print("Remember to set config.developer to False before releasing.")
print()
print("Lint is not a substitute for thorough testing. Remember to update Ren'Py")
print("before releasing. New releases fix bugs and improve compatibility.")
return False