mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-04 06:07:15 -05:00
PR #6438 does 2 things: 1. Add check-namespace 2. Fix verify script This PR contains only part 2 (and does some preparations for part 1). The goal of the PR is to make CI succeed on master.
179 lines
6.5 KiB
Python
Executable File
179 lines
6.5 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
# This script checks for strings like paths or class names that are *not* in the source code, but e.g. in
|
|
# translation files, stylesheets or git files.
|
|
# Invalid strings *in the source code* are usually recognized when you compile them, but other strings may
|
|
# be overseen, which is why this script checks strings *outside of the source code*.
|
|
|
|
|
|
import re
|
|
import subprocess
|
|
import xml.etree.ElementTree as ElementTree
|
|
from pathlib import Path
|
|
import tinycss2
|
|
|
|
|
|
# global variables
|
|
|
|
errors = 0
|
|
|
|
|
|
# functions
|
|
|
|
def caption(my_str):
|
|
print(f'\n# {my_str}\n')
|
|
|
|
def error(where, my_str):
|
|
global errors
|
|
errors += 1
|
|
print(f'Error: {where}: {my_str}')
|
|
|
|
# if a string makes a classname, we need to check if that class is in the source
|
|
# however, some of these strings name classes that are not from LMMS, so these can be ignored for such checks:
|
|
def is_our_class(classname: str) -> int:
|
|
return classname[0] != 'Q' # Qt classes
|
|
|
|
|
|
# prepare some variables
|
|
|
|
if not Path('.gitmodules').is_file():
|
|
print('You need to call this script from the LMMS top directory')
|
|
exit(1)
|
|
|
|
result = subprocess.run(['git', 'ls-files', '*.[ch]', '*.[ch]pp', '*.ui', ':!tests/*'],
|
|
capture_output=True, text=True, check=True)
|
|
files = [Path(f) for f in result.stdout.splitlines()]
|
|
carlabase = 'carlabase' if Path('plugins/carlabase').is_dir() else 'CarlaBase'
|
|
carlapath = f'plugins/{carlabase}/carla/'
|
|
result = subprocess.run(['git',
|
|
'--git-dir', f'{carlapath}/.git',
|
|
'--work-tree', f'{carlapath}',
|
|
'ls-files', 'resources/ui', 'source/frontend'], capture_output=True, text=True, check=True)
|
|
files.extend([Path(f'{carlapath}/{f}') for f in result.stdout.splitlines()])
|
|
|
|
classes = set()
|
|
class_pat = re.compile(r'^\s*class(?:\s+LMMS_EXPORT)?\s+([a-zA-Z_][a-zA-Z0-9_]*)', re.MULTILINE)
|
|
class_pat_ui = re.compile(r'<class>([a-zA-Z_][a-zA-Z0-9_]*)</class>')
|
|
for cur_file in files:
|
|
if cur_file.is_file():
|
|
text = cur_file.read_text(errors='replace')
|
|
classes.update(re.findall(class_pat_ui if cur_file.suffix == '.ui' else class_pat, text))
|
|
|
|
|
|
# the real checks
|
|
|
|
caption('.gitmodules')
|
|
|
|
for p in re.findall(r'\[submodule "([^"]+)"\]\s*$', Path('.gitmodules').read_text(errors='replace'), re.MULTILINE):
|
|
if not Path(p).is_dir():
|
|
error('.gitmodules', f'Directory does not exist: {p}')
|
|
|
|
|
|
caption('locale')
|
|
|
|
filenames = set()
|
|
classes_found = set()
|
|
for cur_file in Path('data/locale').glob('*.ts'):
|
|
tree = ElementTree.parse(str(cur_file))
|
|
root = tree.getroot()
|
|
for location in root.findall('./context/message/location'):
|
|
filenames.add(location.attrib['filename'])
|
|
for location in root.findall('./context/name'):
|
|
classes_found.add(location.text)
|
|
for f in sorted(filenames):
|
|
# The files sometimes are relative to data/local and sometimes to the git tree's root...
|
|
if not Path(f).is_file() and not Path(f'data/locale/{f}').is_file():
|
|
error('data/locale', f'Source file does not exist: {f}')
|
|
for c in sorted(classes_found):
|
|
if is_our_class(c) and '::' not in c and c not in classes:
|
|
error('data/locale', f'Class does not exist in source code: {c}')
|
|
|
|
|
|
caption('themes')
|
|
|
|
|
|
GUI_NAMESPACE_PREFIX = "lmms--gui"
|
|
|
|
|
|
def unscope_classname(stylesheet, cname):
|
|
# Strip the namespace part from the given class name,
|
|
# while expecting it to have one in the first place.
|
|
SCOPE_TOKEN = "--"
|
|
i = cname.rfind(SCOPE_TOKEN) + len(SCOPE_TOKEN)
|
|
|
|
assert i>=0
|
|
|
|
return cname[i:]
|
|
|
|
|
|
for theme in sorted([d for d in Path('data/themes').iterdir() if d.is_dir()]):
|
|
classes_in_sheet = set()
|
|
stylesheet = theme / 'style.css'
|
|
rules = tinycss2.parse_stylesheet(Path(stylesheet).read_text(errors='replace'))
|
|
for rule in rules:
|
|
if rule.type == 'qualified-rule':
|
|
class_found = False
|
|
for c in rule.prelude:
|
|
if c.type == 'ident' and not class_found:
|
|
if is_our_class(c.value):
|
|
if str(c.value).startswith(GUI_NAMESPACE_PREFIX):
|
|
classes_in_sheet.add(unscope_classname(stylesheet, c.value))
|
|
else:
|
|
error(str(stylesheet), f"Namespace prefix missing from class {c.value}")
|
|
class_found = True
|
|
# After whitespace or comma comes a new class
|
|
elif c.type == 'whitespace' or (c.type == 'literal' and c.value == ','):
|
|
class_found = False
|
|
missing_classes = classes_in_sheet - classes
|
|
for class_in_sheet in sorted(missing_classes):
|
|
error(str(stylesheet), f'Class does not exist in source code: {class_in_sheet}')
|
|
|
|
|
|
caption('patches (checks only plugins/)')
|
|
|
|
pat = re.compile(r'/(plugins/\S*)', re.MULTILINE)
|
|
calf = re.compile(r'calf/.*/modules\.') # these are a bit complicated to fix...
|
|
for cur_file in sorted(Path('.').glob('*/patches/*.patch')):
|
|
if Path(cur_file).is_file():
|
|
paths_in_patches = set()
|
|
for line in pat.findall(cur_file.read_text(errors='replace')):
|
|
if not calf.search(line):
|
|
paths_in_patches.add(Path(line))
|
|
for mpath in sorted(paths_in_patches):
|
|
# in case of LADSPA SWH effects, check that the XML exists, not the C file
|
|
# (because the C files are not generated until a build is done)
|
|
if mpath.parent == Path('plugins/LadspaEffect/swh/ladspa/'):
|
|
mpath = mpath.with_suffix('.xml')
|
|
if not mpath.is_file():
|
|
error(str(cur_file), f'Source file does not exist: {str(mpath)}')
|
|
|
|
|
|
caption('debian docs (only one string)')
|
|
|
|
# Checks for caps.html. This gets relevant when #4027 will be merged
|
|
for line in Path('debian/lmms-common.docs').read_text(errors='replace').splitlines():
|
|
line = line.rstrip()
|
|
if 'caps.html' in line and not Path(line).is_file():
|
|
error('debian/lmms-common.docs', f'Path does not exist: {line}')
|
|
|
|
|
|
caption('debian/copyright')
|
|
|
|
pat = re.compile(r'^Files:\s*(\S+).*$', re.MULTILINE)
|
|
ladspa_swh = re.compile(r'(plugins/LadspaEffect/swh/ladspa/[^/.]+)\.c')
|
|
for mpath in pat.findall(Path('debian/copyright').read_text(errors='replace')):
|
|
# in case of LADSPA SWH effects, check that the XML exists, not the C file
|
|
# (because the C files are not generated until a build is done)
|
|
if res2 := ladspa_swh.match(mpath):
|
|
mpath = res2.group(1) + '.xml'
|
|
if not any(Path('.').glob(mpath)):
|
|
error('debian/copyright', f'Glob/Path does not exist: {mpath}')
|
|
|
|
|
|
# summary
|
|
|
|
caption('summary')
|
|
|
|
print(f'{str(errors)} errors.')
|
|
exit(1 if errors > 0 else 0)
|