2 Script to scan the OpenTTD source-tree for STR_ entries that are defined but
5 This is not completely trivial, as OpenTTD references a lot of strings in
6 relation to another string. The most obvious example of this is a list. OpenTTD
7 only references the first entry in the list, and does "+ <var>" to get to the
10 There are other ways OpenTTD does use relative values. This script tries to
11 account for all of them, to give the best approximation we have for "this
23 LENGTH_NAME_LOOKUP
= {
36 def read_language_file(filename
, strings_found
, errors
):
44 with
open(filename
) as fp
:
45 for line
in fp
.readlines():
47 if skip
== SkipType
.EXPECT_NEWLINE
:
53 if skip
== SkipType
.EXPECT_NEWLINE
:
54 # The only thing allowed after a list, is this next marker, or a newline.
55 if line
== "###next-name-looks-similar":
56 # "###next-name-looks-similar"
57 # Indicates the common prefix of the last list has a very
58 # similar name to the next entry, but isn't part of the
59 # list. So do not emit a warning about them looking very
63 errors
.append(f
"ERROR: list around {name} is shorted than indicated by ###length")
67 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?")
72 if line
.startswith("###length "):
74 # Indicates the next few entries are part of a list. Only
75 # the first entry is possibly referenced, and the rest are
79 errors
.append(f
"ERROR: list around {name} is shorted than indicated by ###length")
81 length
= line
.split(" ")[1].strip()
83 if length
.isnumeric():
86 length
= LENGTH_NAME_LOOKUP
[length
]
88 skip
= SkipType
.LENGTH
89 elif line
.startswith("###external "):
90 # "###external <count>"
91 # Indicates the next few entries are used outside the
92 # source and will not be referenced.
95 errors
.append(f
"ERROR: list around {name} is shorted than indicated by ###length")
97 length
= line
.split(" ")[1].strip()
100 skip
= SkipType
.EXTERNAL
101 elif line
.startswith("###setting-zero-is-special"):
102 # "###setting-zero-is-special"
103 # Indicates the next entry is part of the "zero is special"
104 # flag of settings. These entries are not referenced
105 # directly in the code.
108 errors
.append(f
"ERROR: list around {name} is shorted than indicated by ###length")
110 skip
= SkipType
.ZERO_IS_SPECIAL
114 name
= line
.split(":")[0].strip()
115 strings_defined
.append(name
)
117 # If a string ends on _TINY or _SMALL, it can be the {TINY} variant.
118 # Check for this by some fuzzy matching.
119 if name
.endswith(("_SMALL", "_TINY")):
120 last_tiny_string
= name
121 elif last_tiny_string
:
122 matching_name
= "_".join(last_tiny_string
.split("_")[:-1])
123 if name
== matching_name
:
124 strings_found
.add(last_tiny_string
)
126 last_tiny_string
= ""
128 if skip
== SkipType
.EXTERNAL
:
129 strings_found
.add(name
)
130 skip
= SkipType
.LENGTH
132 if skip
== SkipType
.LENGTH
:
136 elif skip
== SkipType
.ZERO_IS_SPECIAL
:
137 strings_found
.add(name
)
139 strings_found
.add(name
)
142 # Find the common prefix of these strings
143 for i
in range(len(common_prefix
)):
144 if common_prefix
[0 : i
+ 1] != name
[0 : i
+ 1]:
145 common_prefix
= common_prefix
[0:i
]
149 skip
= SkipType
.EXPECT_NEWLINE
151 if len(common_prefix
) < 6:
152 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.")
154 if name
.startswith(common_prefix
):
155 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.")
158 return strings_defined
161 def scan_source_files(path
, strings_found
):
162 for new_path
in glob
.glob(f
"{path}/*"):
163 if os
.path
.isdir(new_path
):
164 scan_source_files(new_path
, strings_found
)
167 if not new_path
.endswith((".c", ".h", ".cpp", ".hpp", ".ini")):
170 # Most files we can just open, but some use magic, that requires the
171 # G++ preprocessor before we can make sense out of it.
172 if new_path
== "src/table/cargo_const.h":
173 p
= subprocess
.run(["g++", "-E", new_path
], stdout
=subprocess
.PIPE
)
174 output
= p
.stdout
.decode()
176 with
open(new_path
) as fp
:
179 # Find all the string references.
180 matches
= re
.findall(r
"[^A-Z_](STR_[A-Z0-9_]*)", output
)
181 strings_found
.update(matches
)
185 strings_found
= set()
188 scan_source_files("src", strings_found
)
189 strings_defined
= read_language_file("src/lang/english.txt", strings_found
, errors
)
191 # STR_LAST_STRINGID is special, and not really a string.
192 strings_found
.remove("STR_LAST_STRINGID")
193 # These are mentioned in comments, not really a string.
194 strings_found
.remove("STR_XXX")
195 strings_found
.remove("STR_NEWS")
196 strings_found
.remove("STR_CONTENT_TYPE_")
198 # This string is added for completion, but never used.
199 strings_defined
.remove("STR_JUST_DATE_SHORT")
201 strings_defined
= sorted(strings_defined
)
202 strings_found
= sorted(list(strings_found
))
204 for string
in strings_found
:
205 if string
not in strings_defined
:
206 errors
.append(f
"ERROR: {string} found but never defined.")
208 for string
in strings_defined
:
209 if string
not in strings_found
:
210 errors
.append(f
"ERROR: {string} is (possibly) no longer needed.")
213 print("\n".join(errors
))
219 if __name__
== "__main__":