feat: add auto format fix
This commit is contained in:
parent
6068e9f08a
commit
1a92294fca
1 changed files with 158 additions and 11 deletions
|
|
@ -88,10 +88,15 @@ def import_group(module: str) -> tuple[int, int] | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]:
|
def parse_imports(lines: list[str]) -> tuple[int | None, int | None, list[str], list[tuple[str, int, int, str]]]:
|
||||||
"""Check that module imports are in the required order."""
|
"""Parse the import block, returning (first_idx, last_idx, relative_imports, module_imports).
|
||||||
violations = []
|
|
||||||
imports: list[tuple[int, str, int, int]] = [] # (lineno, module, group, depth)
|
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):
|
for i, line in enumerate(lines):
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
|
|
@ -99,23 +104,49 @@ def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]
|
||||||
continue
|
continue
|
||||||
m = IMPORT_RE.match(stripped)
|
m = IMPORT_RE.match(stripped)
|
||||||
if m:
|
if m:
|
||||||
|
if first_import is None:
|
||||||
|
first_import = i
|
||||||
|
last_import = i
|
||||||
module = m.group(1)
|
module = m.group(1)
|
||||||
result = import_group(module)
|
result = import_group(module)
|
||||||
if result is not None:
|
if result is None:
|
||||||
|
relative_imports.append(line)
|
||||||
|
else:
|
||||||
group, depth = result
|
group, depth = result
|
||||||
imports.append((i + 1, module, group, depth))
|
module_imports.append((line, group, depth, module))
|
||||||
continue
|
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)):
|
for j in range(1, len(imports)):
|
||||||
prev_lineno, prev_mod, prev_group, prev_depth = imports[j - 1]
|
_, prev_line, prev_group, prev_depth, prev_mod = imports[j - 1]
|
||||||
curr_lineno, curr_mod, curr_group, curr_depth = imports[j]
|
_, 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:
|
if curr_group < prev_group:
|
||||||
violations.append(
|
violations.append(
|
||||||
Violation(
|
Violation(
|
||||||
rel,
|
rel,
|
||||||
curr_lineno,
|
lineno,
|
||||||
"import-order",
|
"import-order",
|
||||||
f"'{curr_mod}' should appear before '{prev_mod}'",
|
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(
|
violations.append(
|
||||||
Violation(
|
Violation(
|
||||||
rel,
|
rel,
|
||||||
curr_lineno,
|
lineno,
|
||||||
"import-order",
|
"import-order",
|
||||||
f"'{curr_mod}' should appear before '{prev_mod}' (less nested first)",
|
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
|
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
|
# Regexes
|
||||||
PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s")
|
PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s")
|
||||||
SIGNAL_RE = re.compile(r"^signal\s")
|
SIGNAL_RE = re.compile(r"^signal\s")
|
||||||
|
|
@ -352,8 +494,13 @@ def check_file(filepath: Path) -> list[Violation]:
|
||||||
|
|
||||||
|
|
||||||
def main():
|
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)
|
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")
|
print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n")
|
||||||
|
|
||||||
all_violations: list[Violation] = []
|
all_violations: list[Violation] = []
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue