147 lines
4.8 KiB
Python
Executable File
147 lines
4.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
# Do not load C implementation, so that we can override some parser methods.
|
|
sys.modules["_elementtree"] = None
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
class AnnotatingParser(ET.XMLParser):
|
|
def _start(self, tag, attrs):
|
|
elem = super()._start(tag, attrs)
|
|
elem.line = self.parser.CurrentLineNumber
|
|
elem.col = self.parser.CurrentColumnNumber
|
|
return elem
|
|
|
|
|
|
def log(level, path, el, message):
|
|
return f"::{level} file={path},line={el.line},col={el.col}::{message}"
|
|
|
|
def error(path, el, message):
|
|
return log("error", path, el, message)
|
|
|
|
def warning(path, el, message):
|
|
return log("warning", path, el, message)
|
|
|
|
def notice(path, el, message):
|
|
return log("notice", path, el, message)
|
|
|
|
|
|
def lint(path):
|
|
errors, warnings = 0, 0
|
|
|
|
dirname = os.path.dirname(path)
|
|
db = os.path.basename(dirname) # site, study, ...
|
|
|
|
source = ET.parse(os.path.join(dirname, "..", "..", "source", f"{db}.xml"), parser=AnnotatingParser()).getroot()
|
|
|
|
root = ET.parse(path, parser=AnnotatingParser()).getroot()
|
|
for el in root:
|
|
name = el.attrib["name"]
|
|
if "'" in name or " " in name:
|
|
print(error(path, el, f"bad {el.tag} name: {name}"))
|
|
errors += 1
|
|
continue
|
|
|
|
source_el = source.find(f".//{el.tag}[@name='{name}']")
|
|
|
|
if el.tag == "string":
|
|
errs, warns = lint_string(path, el, name, el.text, source_el.text)
|
|
errors += errs
|
|
warnings += warns
|
|
elif el.tag == "plurals":
|
|
for item in el:
|
|
quantity = item.attrib["quantity"]
|
|
allow_missing = 1 if quantity in ["zero", "one", "two"] else 0
|
|
errs, warns = lint_string(path, item, "{}:{}".format(name, quantity), item.text, source_el.find("./item[@quantity='other']").text, allow_missing)
|
|
errors += errs
|
|
warnings += warns
|
|
else:
|
|
print(error(path, el, f"bad resources tag: {el.tag}"))
|
|
errors += 1
|
|
|
|
return errors, warnings
|
|
|
|
|
|
def lint_string(path, el, name, dest, source, allow_missing=0):
|
|
errs, warns = 0, 0
|
|
|
|
placeholders = source.count("%s")
|
|
if placeholders > 1:
|
|
print(error(path, el, f"more than 1 %s in source: {name} {source}"))
|
|
errs += 1
|
|
|
|
diff = placeholders - dest.count("%s")
|
|
if diff > 0:
|
|
allow_missing -= diff
|
|
print(log("error" if allow_missing < 0 else "warning", path, el, f"missing %s: {name} {dest}"))
|
|
errs += 1 if allow_missing < 0 else 0
|
|
warns += 0 if allow_missing < 0 else 1
|
|
elif diff < 0:
|
|
print(error(path, el, f"too many %s: {name} {dest}"))
|
|
errs += 1
|
|
|
|
for placeholder in re.findall(r"%\d+\$s", source):
|
|
if placeholder == "%1$s" and placeholder not in dest and allow_missing > 0:
|
|
print(warning(path, el, f"missing %1$s: {name} {dest}"))
|
|
allow_missing -= 1
|
|
warns += 1
|
|
elif dest.count(placeholder) < 1:
|
|
print(error(path, el, f"missing {placeholder}: {name} {dest}"))
|
|
errs += 1
|
|
|
|
for placeholder in re.findall(r"%\d+\$s", dest):
|
|
if source.count(placeholder) < 1:
|
|
print(error(path, el, f"unexpected {placeholder}: {name} {dest}"))
|
|
errs += 1
|
|
|
|
for pattern in ["O-O", "SAN", "FEN", "PGN", "K, Q, R, B, N"]:
|
|
m_source = source if pattern.isupper() else source.lower()
|
|
m_dest = dest if pattern.isupper() else dest.lower()
|
|
if pattern in m_source and pattern not in m_dest:
|
|
print(notice(path, el, f"missing {pattern}: {name} {dest}"))
|
|
#elif pattern not in m_source and pattern in m_dest:
|
|
# print(notice(path, el, f"unexpected {pattern}: {name} {dest}"))
|
|
|
|
if "\n" not in source and "\n" in dest:
|
|
print(notice(path, el, f"expected single line string: {name} {dest}"))
|
|
|
|
if re.match(r"\n", dest):
|
|
print(error(path, el, f"has leading newlines: {name} {dest}"))
|
|
errs += 1
|
|
elif re.match(r"\s+", dest):
|
|
print(warning(path, el, f"has leading spaces: {name} {dest}"))
|
|
warns += 1
|
|
|
|
if re.search(r"\s+$", dest):
|
|
print(warning(path, el, f"has trailing spaces: {name} {dest}"))
|
|
warns += 1
|
|
|
|
if re.search(r"\t", dest):
|
|
print(warning(path, el, f"has tabs: {name} {dest}"))
|
|
warns += 1
|
|
|
|
if re.search(r"\n{3,}", dest):
|
|
print(warning(path, el, f"has more than one successive empty line: {name} {dest}"))
|
|
warns += 1
|
|
|
|
return errs, warns
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = sys.argv[1:]
|
|
if args:
|
|
errors, warnings = 0, 0
|
|
for arg in sys.argv[1:]:
|
|
errs, warns = lint(arg)
|
|
errors += errs
|
|
warnings += warns
|
|
print(f"{errors} error(s), {warnings} warning(s)")
|
|
if errors:
|
|
sys.exit(1)
|
|
else:
|
|
print(f"Usage: {sys.argv[0]} translation/dest/*/*.xml")
|