# 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