mirror of
https://github.com/JGRennison/OpenTTD-patches.git
synced 2024-11-17 21:25:40 +00:00
221 lines
7.6 KiB
Python
221 lines
7.6 KiB
Python
"""
|
|
Script to scan the OpenTTD source-tree for STR_ entries that are defined but
|
|
no longer used.
|
|
|
|
This is not completely trivial, as OpenTTD references a lot of strings in
|
|
relation to another string. The most obvious example of this is a list. OpenTTD
|
|
only references the first entry in the list, and does "+ <var>" to get to the
|
|
correct string.
|
|
|
|
There are other ways OpenTTD does use relative values. This script tries to
|
|
account for all of them, to give the best approximation we have for "this
|
|
string is unused".
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
from enum import Enum
|
|
|
|
LENGTH_NAME_LOOKUP = {
|
|
"VEHICLE_TYPES": 4,
|
|
}
|
|
|
|
|
|
class SkipType(Enum):
|
|
NONE = 1
|
|
LENGTH = 2
|
|
EXTERNAL = 3
|
|
ZERO_IS_SPECIAL = 4
|
|
EXPECT_NEWLINE = 5
|
|
|
|
|
|
def read_language_file(filename, strings_found, errors):
|
|
strings_defined = []
|
|
|
|
skip = SkipType.NONE
|
|
length = 0
|
|
common_prefix = ""
|
|
last_tiny_string = ""
|
|
|
|
with open(filename) as fp:
|
|
for line in fp.readlines():
|
|
if not line.strip():
|
|
if skip == SkipType.EXPECT_NEWLINE:
|
|
skip = SkipType.NONE
|
|
continue
|
|
|
|
line = line.strip()
|
|
|
|
if skip == SkipType.EXPECT_NEWLINE:
|
|
# The only thing allowed after a list, is this next marker, or a newline.
|
|
if line == "###next-name-looks-similar":
|
|
# "###next-name-looks-similar"
|
|
# Indicates the common prefix of the last list has a very
|
|
# similar name to the next entry, but isn't part of the
|
|
# list. So do not emit a warning about them looking very
|
|
# similar.
|
|
|
|
if length != 0:
|
|
errors.append(f"ERROR: list around {name} is shorted than indicated by ###length")
|
|
|
|
common_prefix = ""
|
|
else:
|
|
errors.append(f"ERROR: expected a newline after a list, but didn't find any around {name}. Did you add an entry to the list without increasing the length?")
|
|
|
|
skip = SkipType.NONE
|
|
|
|
if line[0] == "#":
|
|
if line.startswith("###length "):
|
|
# "###length <count>"
|
|
# Indicates the next few entries are part of a list. Only
|
|
# the first entry is possibly referenced, and the rest are
|
|
# indirectly.
|
|
|
|
if length != 0:
|
|
errors.append(f"ERROR: list around {name} is shorted than indicated by ###length")
|
|
|
|
length = line.split(" ")[1].strip()
|
|
|
|
if length.isnumeric():
|
|
length = int(length)
|
|
else:
|
|
length = LENGTH_NAME_LOOKUP[length]
|
|
|
|
skip = SkipType.LENGTH
|
|
elif line.startswith("###external "):
|
|
# "###external <count>"
|
|
# Indicates the next few entries are used outside the
|
|
# source and will not be referenced.
|
|
|
|
if length != 0:
|
|
errors.append(f"ERROR: list around {name} is shorted than indicated by ###length")
|
|
|
|
length = line.split(" ")[1].strip()
|
|
length = int(length)
|
|
|
|
skip = SkipType.EXTERNAL
|
|
elif line.startswith("###setting-zero-is-special"):
|
|
# "###setting-zero-is-special"
|
|
# Indicates the next entry is part of the "zero is special"
|
|
# flag of settings. These entries are not referenced
|
|
# directly in the code.
|
|
|
|
if length != 0:
|
|
errors.append(f"ERROR: list around {name} is shorted than indicated by ###length")
|
|
|
|
skip = SkipType.ZERO_IS_SPECIAL
|
|
|
|
continue
|
|
|
|
name = line.split(":")[0].strip()
|
|
strings_defined.append(name)
|
|
|
|
# If a string ends on _TINY or _SMALL, it can be the {TINY} variant.
|
|
# Check for this by some fuzzy matching.
|
|
if name.endswith(("_SMALL", "_TINY")):
|
|
last_tiny_string = name
|
|
elif last_tiny_string:
|
|
matching_name = "_".join(last_tiny_string.split("_")[:-1])
|
|
if name == matching_name:
|
|
strings_found.add(last_tiny_string)
|
|
else:
|
|
last_tiny_string = ""
|
|
|
|
if skip == SkipType.EXTERNAL:
|
|
strings_found.add(name)
|
|
skip = SkipType.LENGTH
|
|
|
|
if skip == SkipType.LENGTH:
|
|
skip = SkipType.NONE
|
|
length -= 1
|
|
common_prefix = name
|
|
elif skip == SkipType.ZERO_IS_SPECIAL:
|
|
strings_found.add(name)
|
|
elif length > 0:
|
|
strings_found.add(name)
|
|
length -= 1
|
|
|
|
# Find the common prefix of these strings
|
|
for i in range(len(common_prefix)):
|
|
if common_prefix[0 : i + 1] != name[0 : i + 1]:
|
|
common_prefix = common_prefix[0:i]
|
|
break
|
|
|
|
if length == 0:
|
|
skip = SkipType.EXPECT_NEWLINE
|
|
|
|
if len(common_prefix) < 6:
|
|
errors.append(f"ERROR: common prefix of block including {name} was reduced to {common_prefix}. This means the names in the list are not consistent.")
|
|
elif common_prefix:
|
|
if name.startswith(common_prefix):
|
|
errors.append(f"ERROR: {name} looks a lot like block above with prefix {common_prefix}. This mostly means that the list length was too short. Use '###next-name-looks-similar' if it is not.")
|
|
common_prefix = ""
|
|
|
|
return strings_defined
|
|
|
|
|
|
def scan_source_files(path, strings_found):
|
|
for new_path in glob.glob(f"{path}/*"):
|
|
if os.path.isdir(new_path):
|
|
scan_source_files(new_path, strings_found)
|
|
continue
|
|
|
|
if not new_path.endswith((".c", ".h", ".cpp", ".hpp", ".ini")):
|
|
continue
|
|
|
|
# Most files we can just open, but some use magic, that requires the
|
|
# G++ preprocessor before we can make sense out of it.
|
|
if new_path == "src/table/cargo_const.h":
|
|
p = subprocess.run(["g++", "-E", new_path], stdout=subprocess.PIPE)
|
|
output = p.stdout.decode()
|
|
else:
|
|
with open(new_path) as fp:
|
|
output = fp.read()
|
|
|
|
# Find all the string references.
|
|
matches = re.findall(r"[^A-Z_](STR_[A-Z0-9_]*)", output)
|
|
strings_found.update(matches)
|
|
|
|
|
|
def main():
|
|
strings_found = set()
|
|
errors = []
|
|
|
|
scan_source_files("src", strings_found)
|
|
strings_defined = read_language_file("src/lang/english.txt", strings_found, errors)
|
|
|
|
# STR_LAST_STRINGID is special, and not really a string.
|
|
strings_found.remove("STR_LAST_STRINGID")
|
|
# These are mentioned in comments, not really a string.
|
|
strings_found.remove("STR_XXX")
|
|
strings_found.remove("STR_NEWS")
|
|
strings_found.remove("STR_CONTENT_TYPE_")
|
|
|
|
# This string is added for completion, but never used.
|
|
strings_defined.remove("STR_JUST_DATE_SHORT")
|
|
|
|
strings_defined = sorted(strings_defined)
|
|
strings_found = sorted(list(strings_found))
|
|
|
|
for string in strings_found:
|
|
if string not in strings_defined:
|
|
errors.append(f"ERROR: {string} found but never defined.")
|
|
|
|
for string in strings_defined:
|
|
if string not in strings_found:
|
|
errors.append(f"ERROR: {string} is (possibly) no longer needed.")
|
|
|
|
if errors:
|
|
print("\n".join(errors))
|
|
sys.exit(1)
|
|
|
|
print("OK")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|