llhttp/check-null-licenses

192 lines
5.7 KiB
Plaintext
Raw Normal View History

2021-12-06 19:53:52 +00:00
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import json
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
from pathlib import Path
from sys import exit, stderr
import tomllib
2021-12-06 19:53:52 +00:00
def main():
args = parse_args()
problem = False
if not args.tree.is_dir():
return f"Not a directory: {args.tree}"
for pjpath in args.tree.glob("**/package.json"):
name, version, license = parse(pjpath)
identity = f"{name} {version}"
if version in args.exceptions.get(name, ()):
continue # Do not even check the license
elif license is None:
problem = True
print(
f"Missing license in package.json for {identity}", file=stderr
)
elif isinstance(license, dict):
if isinstance(license.get("type"), str):
continue
print(
(
"Missing type for (deprecated) license object in "
f"package.json for {identity}: {license}"
),
file=stderr,
)
elif isinstance(license, list):
if license and all(
isinstance(entry, dict) and isinstance(entry.get("type"), str)
for entry in license
):
continue
print(
(
"Defective (deprecated) licenses array-of objects in "
f"package.json for {identity}: {license}"
),
file=stderr,
)
elif isinstance(license, str):
continue
else:
print(
(
"Weird type for license in "
f"package.json for {identity}: {license}"
),
file=stderr,
)
problem = True
if problem:
return "At least one missing license was found."
def check_exception(exceptions, name, version):
x = args.exceptions
def parse(package_json_path):
with package_json_path.open("rb") as pjfile:
pj = json.load(pjfile)
try:
license = pj["license"]
except KeyError:
license = pj.get("licenses")
try:
name = pj["name"]
except KeyError:
name = package_json_path.parent.name
version = pj.get("version", "<unknown version>")
return name, version, license
def parse_args():
parser = ArgumentParser(
formatter_class=RawDescriptionHelpFormatter,
description=(
"Search for bundled dependencies without declared licenses"
),
epilog="""
The exceptions file must be a TOML file with zero or more tables. Each tables
keys are package names; the corresponding values values are exact version
number strings, or arrays of version number strings, that have been manually
audited to determine their license status and should therefore be ignored.
Exceptions in a table called “any” are always applied. Otherwise, exceptions
are applied only if a corresponding --with TABLENAME argument is given;
multiple such arguments may be given.
For
example:
[any]
example-foo = "1.0.0"
[prod]
example-bar = [ "2.0.0", "2.0.1",]
[dev]
example-bat = [ "3.7.4",]
would always ignore version 1.0.0 of example-foo. It would ignore example-bar
2.0.1 only when called with “--with prod”.
Comments may (and should) be used to describe the manual audits upon which the
exclusions are based.
Otherwise, any package.json with missing or null license field in the tree is
considered an error, and the program returns with nonzero status.
""",
)
parser.add_argument(
"-x",
"--exceptions",
type=FileType("rb"),
2021-12-06 19:53:52 +00:00
help="Manually audited package versions file",
)
parser.add_argument(
"-w",
"--with",
action="append",
default=[],
help="Enable a table in the exceptions file",
)
parser.add_argument(
"tree",
metavar="node_modules_dir",
type=Path,
help="Path to search recursively",
default=".",
)
args = parser.parse_args()
if args.exceptions is None:
args.exceptions = {}
xname = None
else:
with args.exceptions as xfile:
xname = getattr(xfile, "name", "<exceptions>")
args.exceptions = tomllib.load(args.exceptions)
2021-12-06 19:53:52 +00:00
if not isinstance(args.exceptions, dict):
parser.error(f"Invalid format in {xname}: not an object")
for tablename, table in args.exceptions.items():
if not isinstance(table, dict):
parser.error(
f"Non-table entry in {xname}: {tablename} = {table!r}"
)
overlay = {}
for key, value in table.items():
if isinstance(value, str):
overlay[key] = [value]
elif not isinstance(value, list) or not all(
isinstance(entry, str) for entry in value
):
parser.error(
f"Invalid format in {xname} in [{tablename}]: "
f"{key!r} = {value!r}"
)
table.update(overlay)
x = args.exceptions.get("any", {})
for add in getattr(args, "with"):
try:
x.update(args.exceptions[add])
except KeyError:
if xname is None:
parser.error(
f"No table {add}, as no exceptions file was given"
)
else:
parser.error(f"No table {add} in {xname}")
# Store the merged dictionary
args.exceptions = x
return args
if __name__ == "__main__":
exit(main())