CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 15s
Android / Core and FFI clippy and tests (push) Successful in 27s
CI & Build / integration (push) Successful in 1m39s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m55s
Android / Build the server image (push) Successful in 6s
Milestone 546 "Bring the repo to the comment standard (rule 32) and compose defaults (rule 198)", step #6223 "Comment standard sweep: Android client". KDoc, comments, manifest and resource notes, Gradle and TOML comments and the ffi crate's docs now state what the code does and the non-obvious why in the present tense. Issue, Scribe-note, milestone (M13, M304, M315, M12) and family-idea references are gone, as are comparisons to Minstrel and the narration of what used to be: the capture sheet the editor replaced, the per-note colour story, the operator's device-pass feedback, the bindgen-as-[[bin]] failure, the 0.60 edge alpha before it was re-solved, and the checker bugs behind check-strings/check-symbols. Hazards stay as facts: the signing key that can't be rotated, the alias that keeps the earlier name, PKCS12's single password, unwind over abort, the UTC date-picker read, the DerivedTint/colors.ts mirror. One stale claim is gone: DerivedTint said the web had no test runner, but notes/grammar.test.ts runs the same tint cases. detekt.yml said "these two places" over a list of five. 49 files, comments only: a comment-stripping diff of every changed file against HEAD is identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
3.6 KiB
Python
Executable File
111 lines
3.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Check every R.string / R.plurals reference against strings.xml.
|
|
|
|
Three ways a resource reference compiles and then fails, none of which ktlint,
|
|
detekt or the Kotlin compiler will catch:
|
|
|
|
1. the name does not exist -> resource-not-found at runtime
|
|
2. `stringResource` on a plural (or the reverse) -> wrong overload, wrong text
|
|
3. the format string takes more arguments than the call passes -> the format
|
|
silently renders `%2$s` as literal text, or throws
|
|
|
|
python3 android/tools/check-strings.py
|
|
|
|
Exits non-zero on any problem.
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
BASE = (
|
|
sys.argv[1]
|
|
if len(sys.argv) > 1
|
|
else os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
|
)
|
|
STRINGS = os.path.join(BASE, "app", "src", "main", "res", "values", "strings.xml")
|
|
SOURCES = os.path.join(BASE, "app", "src", "main", "java", "**", "*.kt")
|
|
|
|
CALL = re.compile(
|
|
r"(stringResource|pluralStringResource)\(\s*R\.(string|plurals)\.(\w+)"
|
|
r"((?:[^()]|\([^()]*\))*)\)"
|
|
)
|
|
|
|
|
|
def arity(text):
|
|
"""How many distinct arguments a format string consumes."""
|
|
numbered = set(re.findall(r"%(\d)\$", text))
|
|
return len(numbered) if numbered else len(re.findall(r"%[sd]", text))
|
|
|
|
|
|
def supplied_args(rest):
|
|
"""Count top-level commas in an argument tail.
|
|
|
|
Kotlin permits a TRAILING comma before the closing paren, which is not an
|
|
argument; counting it would inflate every multi-line call by one. Braces count
|
|
toward depth as well as parens, or a comma inside a lambda would be read as
|
|
another argument.
|
|
"""
|
|
rest = rest.rstrip()
|
|
if rest.endswith(","):
|
|
rest = rest[:-1]
|
|
depth = 0
|
|
count = 0
|
|
for ch in rest:
|
|
if ch in "([{":
|
|
depth += 1
|
|
elif ch in ")]}":
|
|
depth -= 1
|
|
elif ch == "," and depth == 0:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
def main():
|
|
root = ET.parse(STRINGS).getroot()
|
|
strings = {e.get("name"): "".join(e.itertext()) for e in root.findall("string")}
|
|
plurals = {
|
|
e.get("name"): max(
|
|
(arity("".join(i.itertext())) for i in e.findall("item")), default=0
|
|
)
|
|
for e in root.findall("plurals")
|
|
}
|
|
|
|
problems = 0
|
|
for path in glob.glob(SOURCES, recursive=True):
|
|
with open(path, encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
for match in CALL.finditer(src):
|
|
fn, kind, name, rest = match.groups()
|
|
line = src[: match.start()].count("\n") + 1
|
|
where = f"{os.path.basename(path)}:{line} {name}"
|
|
|
|
if kind == "string" and name not in strings:
|
|
print(f"MISSING {where}: no such string")
|
|
problems += 1
|
|
continue
|
|
if kind == "plurals" and name not in plurals:
|
|
print(f"MISSING {where}: no such plural")
|
|
problems += 1
|
|
continue
|
|
if (fn == "pluralStringResource") != (kind == "plurals"):
|
|
print(f"KIND {where}: {fn} used on R.{kind}")
|
|
problems += 1
|
|
continue
|
|
|
|
passed = supplied_args(rest)
|
|
# A plural call passes the count first, then the format arguments.
|
|
wanted = arity(strings[name]) if kind == "string" else plurals[name] + 1
|
|
if passed != wanted:
|
|
print(f"ARITY {where}: wants {wanted}, call passes {passed}")
|
|
problems += 1
|
|
|
|
print(f"\n{len(strings)} strings, {len(plurals)} plurals, {problems} problems")
|
|
return 1 if problems else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|