#!/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'([a-zA-Z_][a-zA-Z0-9_]*)') 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)