feat: add auto format fix

This commit is contained in:
2 * r + 2 * t 2026-03-22 06:01:59 +11:00
parent 6068e9f08a
commit 1a92294fca

View file

@ -88,10 +88,15 @@ def import_group(module: str) -> tuple[int, int] | None:
return None
def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]:
"""Check that module imports are in the required order."""
violations = []
imports: list[tuple[int, str, int, int]] = [] # (lineno, module, group, depth)
def parse_imports(lines: list[str]) -> tuple[int | None, int | None, list[str], list[tuple[str, int, int, str]]]:
"""Parse the import block, returning (first_idx, last_idx, relative_imports, module_imports).
module_imports entries are (line_text, group, depth, module_name).
"""
first_import = None
last_import = None
relative_imports: list[str] = []
module_imports: list[tuple[str, int, int, str]] = []
for i, line in enumerate(lines):
stripped = line.strip()
@ -99,23 +104,49 @@ def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]
continue
m = IMPORT_RE.match(stripped)
if m:
if first_import is None:
first_import = i
last_import = i
module = m.group(1)
result = import_group(module)
if result is not None:
if result is None:
relative_imports.append(line)
else:
group, depth = result
imports.append((i + 1, module, group, depth))
module_imports.append((line, group, depth, module))
continue
break # end of import block
break
return first_import, last_import, relative_imports, module_imports
def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]:
"""Check that module imports are in the required order."""
violations = []
_, _, _, module_imports = parse_imports(lines)
imports = [(i, *entry) for i, entry in enumerate(module_imports)]
for j in range(1, len(imports)):
prev_lineno, prev_mod, prev_group, prev_depth = imports[j - 1]
curr_lineno, curr_mod, curr_group, curr_depth = imports[j]
_, prev_line, prev_group, prev_depth, prev_mod = imports[j - 1]
_, curr_line, curr_group, curr_depth, curr_mod = imports[j]
# Find actual line number for the current import
lineno = 0
count = 0
for li, line in enumerate(lines):
stripped = line.strip()
m = IMPORT_RE.match(stripped)
if m and import_group(m.group(1)) is not None:
if count == j:
lineno = li + 1
break
count += 1
if curr_group < prev_group:
violations.append(
Violation(
rel,
curr_lineno,
lineno,
"import-order",
f"'{curr_mod}' should appear before '{prev_mod}'",
)
@ -124,7 +155,7 @@ def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]
violations.append(
Violation(
rel,
curr_lineno,
lineno,
"import-order",
f"'{curr_mod}' should appear before '{prev_mod}' (less nested first)",
)
@ -133,6 +164,117 @@ def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]
return violations
def fix_imports(lines: list[str]) -> list[str]:
"""Sort imports and return the modified lines."""
first, last, relative, module = parse_imports(lines)
if first is None:
return lines
module.sort(key=lambda x: (x[1], x[2], x[3]))
sorted_imports = relative + [entry[0] for entry in module]
return lines[:first] + sorted_imports + lines[last + 1 :]
def fix_section_separators(lines: list[str]) -> list[str]:
"""Insert blank lines between different sections and return modified lines."""
insertions: list[int] = []
scopes: dict[str, ScopeTracker] = {}
in_block_comment = False
func_skip_depth = 0
prev_blank: dict[str, bool] = {}
for i, line in enumerate(lines):
stripped = line.strip()
indent = get_indent(line)
if in_block_comment:
if BLOCK_COMMENT_END.search(stripped):
in_block_comment = False
continue
if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped):
in_block_comment = True
continue
if not stripped:
for key in prev_blank:
prev_blank[key] = True
continue
if COMMENT_LINE_RE.match(stripped):
continue
if func_skip_depth > 0:
func_skip_depth += stripped.count("{") - stripped.count("}")
if func_skip_depth <= 0:
func_skip_depth = 0
continue
if stripped == "}":
to_remove = [k for k in scopes if len(k) > len(indent)]
for k in to_remove:
del scopes[k]
prev_blank.pop(k, None)
continue
section = classify_line(stripped)
if section is None:
continue
if indent not in scopes:
scopes[indent] = ScopeTracker()
prev_blank[indent] = True
tracker = scopes[indent]
had_blank = prev_blank.get(indent, True)
if tracker.last_section is not None and section != tracker.last_section and not had_blank:
insertions.append(i)
if tracker.last_section is None or section >= tracker.last_section:
tracker.last_section = section
tracker.last_section_line = i + 1
prev_blank[indent] = False
brace_count = stripped.count("{") - stripped.count("}")
if brace_count > 0 and section == Section.FUNCTION:
func_skip_depth = brace_count
if brace_count > 0 and section == Section.BINDING:
colon_idx = stripped.index(":")
after_colon = stripped[colon_idx + 1 :].strip()
if not re.match(r"^[A-Z]", after_colon):
func_skip_depth = brace_count
if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF):
to_remove = [k for k in scopes if len(k) > len(indent)]
for k in to_remove:
del scopes[k]
prev_blank.pop(k, None)
result = list(lines)
for idx in reversed(insertions):
result.insert(idx, "")
return result
def fix_file(filepath: Path) -> bool:
"""Fix auto-fixable violations. Returns True if file was modified."""
try:
text = filepath.read_text()
except (OSError, UnicodeDecodeError):
return False
lines = text.splitlines()
lines = fix_imports(lines)
lines = fix_section_separators(lines)
new_text = "\n".join(lines)
if text.endswith("\n"):
new_text += "\n"
if new_text != text:
filepath.write_text(new_text)
return True
return False
# Regexes
PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s")
SIGNAL_RE = re.compile(r"^signal\s")
@ -352,8 +494,13 @@ def check_file(filepath: Path) -> list[Violation]:
def main():
fix_mode = "--fix" in sys.argv
qml_files = sorted(p for p in REPO_ROOT.rglob("*.qml") if "build" not in p.parts)
if fix_mode:
fixed = sum(1 for f in qml_files if fix_file(f))
print(f"{BOLD}Fixed {fixed} file(s).{RESET}\n")
print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n")
all_violations: list[Violation] = []