blob: fd7a5d696728d9f4590d6edd52a75cab286248e7 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2017 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Linter for various README.md files."""
import difflib
import logging
import os
from pathlib import Path
import re
_HACK_VAR_TO_DISABLE_ISORT = "hack"
# pylint: disable=wrong-import-position
import chromite_init # pylint: disable=unused-import
from chromite.lib import commandline
from chromite.lib import git
from chromite.lib import osutils
TOP_DIR = Path(__file__).resolve().parent.parent
def GetActiveProjects():
"""Return the list of active projects."""
# Look at all the paths (files & dirs) in the top of the git repo. This way
# we ignore local directories devs created that aren't actually committed.
cmd = ["ls-tree", "--name-only", "-z", "HEAD"]
result = git.RunGit(TOP_DIR, cmd)
# Split the output on NULs to avoid whitespace/etc... issues.
paths = result.stdout.split("\0")
# ls-tree -z will include a trailing NUL on all entries, not just
# separation, so filter it out if found (in case ls-tree behavior changes
# on us).
for path in [x for x in paths if x]:
if os.path.isdir(os.path.join(TOP_DIR, path)):
yield path
def CheckTopLevel():
"""Check the top level README.md list."""
ret = 0
path = os.path.join(TOP_DIR, "README.md")
data = osutils.ReadFile(path).splitlines()
# Look for the directory header.
try:
i = data.index("# Local Project Directory")
except ValueError:
logging.error("README.md index out of sync")
return 1
data = data[i + 1 :]
# Pull out all the linked projects.
listed_projs = []
listed_dirs = []
for line in data:
# Break once we hit the end of the table.
if line.startswith("#"):
break
m = re.match(r"^[|] \[([^]]+)*\]\(\./([^)]*)/\)", line)
if m:
listed_projs.append(m.group(1))
listed_dirs.append(m.group(2))
logging.debug("README.md projects: %s", listed_projs)
sorted_projs = sorted(listed_projs)
if listed_projs != sorted_projs:
ret = 1
lines = list(
difflib.unified_diff(listed_projs, sorted_projs, lineterm="")
)
logging.error(
"README.md project listing should be kept sorted:\n%s",
"\n".join(lines[2:]),
)
# Check the list in README.md for outdated entries.
old_dirs = []
for project_dir in listed_dirs:
path = os.path.join(TOP_DIR, project_dir)
if not os.path.isdir(path):
old_dirs.append(path)
if old_dirs:
ret = 1
logging.error("README.md: found stale project entries: %s", old_dirs)
# Check the local source repo for missing entries.
existing_projs = sorted(GetActiveProjects())
logging.debug("Found active projects: %s", existing_projs)
new_projs = []
for proj in existing_projs:
path = os.path.join(TOP_DIR, proj)
if os.path.isdir(path) and proj not in listed_projs:
new_projs.append(proj)
if new_projs:
ret = 1
logging.error("README.md: document new projects: %s", new_projs)
return ret
def CheckSubdirs():
"""Check the subdir README.md files exist."""
# Legacy projects that don't have a README.md file.
# Someone should write some docs :D.
LEGACYLIST = (
"attestation", # TODO(b/262375413)
"avtest_label_detect",
"cros-disks", # TODO(b/262376624)
"image-burner",
"init",
"libchromeos-ui",
"libcontainer",
"modem-utilities",
"mtpd", # TODO(b/262376388)
"timberslide",
"tpm_manager", # TODO(b/262375897)
"trim",
)
ret = 0
for proj in GetActiveProjects():
readme = os.path.join(TOP_DIR, proj, "README.md")
if os.path.exists(readme):
if proj in LEGACYLIST:
logging.error(
'*** Project "%s" is in no-README LEGACYLIST, but '
"actually has one. Please remove it from "
"LEGACYLIST!",
proj,
)
else:
if not proj in LEGACYLIST:
logging.error('*** Project "%s" needs a README.md file', proj)
ret = 1
# Make sure the list doesn't get stale itself.
old_projects = set(LEGACYLIST) - set(GetActiveProjects())
if old_projects:
logging.error(
"*** %s:LEGACYLIST contains old entries %s. Please remove them.",
__file__,
old_projects,
)
ret = 1
return ret
def GetParser():
"""Return an argument parser."""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument(
"--extensions",
default="gyp,gypi",
help="Comma delimited file extensions to check. "
"(default: %(default)s)",
)
parser.add_argument("files", nargs="*", help="Files to run lint.")
return parser
def main(argv):
parser = GetParser()
opts = parser.parse_args(argv)
opts.Freeze()
return CheckTopLevel() | CheckSubdirs()
if __name__ == "__main__":
commandline.ScriptWrapperMain(lambda _: main)