179 lines
5.6 KiB
Python
179 lines
5.6 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from mozlint import result
|
|
from mozlint.pathutils import expand_exclusions
|
|
|
|
repo_root = Path(__file__).parents[3]
|
|
mach_site_file = repo_root / "python" / "sites" / "mach.txt"
|
|
taskgraph_requirements_file = repo_root / "taskcluster" / "requirements.in"
|
|
skip_prefixes = ("#", "requires-python:")
|
|
|
|
TASKGRAPH_VERSION_RE = re.compile(r"taskcluster-taskgraph(?:\[.*?\])?==(\S+?)(?::|$)")
|
|
|
|
|
|
def lint(paths, config, fix=None, **lintargs):
|
|
results = []
|
|
fixed = 0
|
|
files = sorted(Path(p) for p in expand_exclusions(paths, config, lintargs["root"]))
|
|
log = lintargs["log"]
|
|
|
|
mach_dependencies = set()
|
|
for raw_line in mach_site_file.read_text().splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith(skip_prefixes):
|
|
continue
|
|
mach_dependencies.add(line)
|
|
|
|
for path in files:
|
|
try:
|
|
if path.name != "mach.txt":
|
|
redundant_spec_results, redundant_spec_fixed = (
|
|
command_site_redundant_specifications(
|
|
path, mach_dependencies, fix, config
|
|
)
|
|
)
|
|
results += redundant_spec_results
|
|
fixed += redundant_spec_fixed
|
|
|
|
results += check_taskgraph_version_sync(path, config)
|
|
|
|
order_results, order_fixed = site_ordering(path, fix, config)
|
|
results += order_results
|
|
fixed += order_fixed
|
|
|
|
except Exception as ex:
|
|
log.debug(f"Error: {ex}, in file: {path}")
|
|
|
|
return {"results": results, "fixed": fixed}
|
|
|
|
|
|
def command_site_redundant_specifications(
|
|
path: Path, mach_dependencies: set, fix: bool, config
|
|
):
|
|
lines = path.read_text().splitlines(keepends=True)
|
|
new_lines = []
|
|
fixed = 0
|
|
results = []
|
|
|
|
for line_number, raw_line in enumerate(lines):
|
|
stripped = raw_line.strip()
|
|
if not stripped or stripped.startswith(skip_prefixes):
|
|
new_lines.append(raw_line)
|
|
continue
|
|
|
|
if stripped in mach_dependencies:
|
|
if fix:
|
|
fixed += 1
|
|
continue
|
|
|
|
output = {
|
|
"path": str(path),
|
|
"message": f"Specification of '{stripped}' is redundant; already in {mach_site_file}.",
|
|
"level": "error",
|
|
"lineno": line_number + 1,
|
|
}
|
|
results.append(result.from_config(config, **output))
|
|
else:
|
|
new_lines.append(raw_line)
|
|
|
|
if fixed:
|
|
path.write_text("".join(new_lines), newline="\n")
|
|
|
|
return results, fixed
|
|
|
|
|
|
def site_ordering(path: Path, fix: bool, config):
|
|
lines = path.read_text().splitlines(keepends=True)
|
|
first_line = lines[0]
|
|
results = []
|
|
fixed = 0
|
|
start_index = 1
|
|
|
|
if not first_line.strip().startswith("requires-python:"):
|
|
start_index = 0
|
|
results.append(
|
|
result.from_config(
|
|
config,
|
|
path=str(path),
|
|
message="First line must start with 'requires-python:'.",
|
|
level="error",
|
|
lineno=1,
|
|
)
|
|
)
|
|
|
|
blocks = []
|
|
comment_block = []
|
|
for raw_line in lines[start_index:]:
|
|
stripped = raw_line.strip()
|
|
if not stripped:
|
|
continue
|
|
|
|
# Group any comment lines with the following specification line before sorting.
|
|
if stripped.startswith("#"):
|
|
comment_block.append(raw_line)
|
|
else:
|
|
blocks.append((comment_block.copy(), raw_line))
|
|
comment_block.clear()
|
|
|
|
sorted_blocks = sorted(blocks, key=lambda pair: pair[1].strip())
|
|
if blocks != sorted_blocks:
|
|
if fix:
|
|
new_lines = [first_line]
|
|
for comments, spec in sorted_blocks:
|
|
new_lines.extend(comments)
|
|
new_lines.append(spec if spec.endswith("\n") else spec + "\n")
|
|
path.write_text("".join(new_lines), newline="\n")
|
|
fixed = 1
|
|
else:
|
|
results.append(
|
|
result.from_config(
|
|
config,
|
|
path=str(path),
|
|
message="After 'requires-python:', entries must be in alphabetical order.",
|
|
hint="Re-run with --fix to resolve this automatically.",
|
|
level="error",
|
|
lineno=start_index + 1,
|
|
)
|
|
)
|
|
|
|
return results, fixed
|
|
|
|
|
|
def check_taskgraph_version_sync(site_path: Path, config):
|
|
results = []
|
|
|
|
site_version = site_lineno = None
|
|
for lineno, line in enumerate(site_path.read_text().splitlines(), start=1):
|
|
m = TASKGRAPH_VERSION_RE.search(line)
|
|
if m:
|
|
site_version, site_lineno = m.group(1), lineno
|
|
break
|
|
|
|
if not site_version:
|
|
return results
|
|
|
|
for line in taskgraph_requirements_file.read_text().splitlines():
|
|
m = TASKGRAPH_VERSION_RE.search(line)
|
|
if m and m.group(1) != site_version:
|
|
results.append(
|
|
result.from_config(
|
|
config,
|
|
path=str(site_path),
|
|
message=(
|
|
f"taskcluster-taskgraph version mismatch: "
|
|
f"{site_path.name} has '{site_version}' but "
|
|
f"taskcluster/requirements.in has '{m.group(1)}'."
|
|
),
|
|
level="error",
|
|
lineno=site_lineno,
|
|
)
|
|
)
|
|
break
|
|
|
|
return results
|