blob: 8083ddb493900d41669a1e1a160918fec1c60701 [file] [log] [blame]
# Copyright 2018 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import difflib
import functools
import json
import pathlib
import yaml
from typing import Sequence, Optional, List, Any, Iterator
class File:
"""Wrapper class for file definitions."""
def __init__(self, json) -> None:
self.source: str = json["source"]
self.destination: str = json["destination"]
def __str__(self) -> str:
return "{%s <-- %s}" % (self.destination, self.source)
@functools.total_ordering
class Atom(object):
"""Wrapper class for atom data, adding convenience methods."""
def __init__(self, json) -> None:
self.json = json
self.id: str = json["id"]
self.metadata: str = json["meta"]
self.label: str = json["gn-label"]
self.category: str = json["category"]
self.area: Optional[str] = json["area"]
self.deps: Sequence[str] = json["deps"]
self.files: Sequence[File] = [File(f) for f in json["files"]]
self.type: str = json["type"]
def __str__(self):
return str(self.id)
def __hash__(self):
return hash(self.label)
def __eq__(self, other):
return self.label == other.label
def __ne__(self, other):
return not __eq__(self, other)
def __lt__(self, other):
return self.id < other.id
def gather_dependencies(manifests):
"""Extracts the set of all required atoms from the given manifests, as well
as the set of names of all the direct dependencies.
"""
direct_deps = set()
atoms = set()
if manifests is None:
return (direct_deps, atoms)
for dep in manifests:
with open(dep, "r") as dep_file:
dep_manifest = json.load(dep_file)
direct_deps.update(dep_manifest["ids"])
atoms.update([Atom(a) for a in dep_manifest["atoms"]])
return (direct_deps, atoms)
def detect_collisions(atoms: Sequence[Atom]) -> Iterator[str]:
"""Detects name collisions in a given atom list. Yields a series of error
messages as strings."""
mappings = collections.defaultdict(lambda: [])
for atom in atoms:
mappings[atom.id].append(atom)
for id, group in mappings.items():
if len(group) == 1:
continue
labels = [a.label for a in group]
msg = "Targets sharing the SDK id %s:\n" % id
for label in labels:
msg += " - %s\n" % label
yield msg
CATEGORIES = [
"excluded",
"experimental",
"internal",
"cts",
"partner_internal",
"partner",
"public",
]
def _index_for_category(category: str):
if not category in CATEGORIES:
raise Exception('Unknown SDK category "%s"' % category)
return CATEGORIES.index(category)
def detect_category_violations(
category: str, atoms: Sequence[Atom]
) -> Iterator[str]:
"""Yields strings describing mismatches in publication categories."""
category_index = _index_for_category(category)
for atom in atoms:
if _index_for_category(atom.category) < category_index:
yield (
"%s has publication level %s, incompatible with %s"
% (atom, atom.category, category)
)
def area_names_from_file(parsed_areas: Any) -> List[str]:
"""Given a parsed version of docs/contribute/governance/areas/_areas.yaml,
return a list of acceptable area names."""
return [area["name"] for area in parsed_areas] + ["Unknown"]
_AREA_REQUIRED_CATEGORIES = ["public", "partner", "partner_internal"]
_AREA_OPTIONAL_TYPES = [
"cc_prebuilt_library",
"cc_source_library",
"companion_host_tool",
"dart_library",
"data",
"documentation",
"experimental_python_e2e_test",
"ffx_tool",
"host_tool",
"loadable_module",
"package",
"sysroot",
"version_history",
]
class Validator:
"""Helper class to validate sets of IDK atoms."""
def __init__(self, valid_areas: Sequence[str]) -> None:
"""Construct a validator with a given set of areas. Exposed for
testing. Use Validator.from_areas_file_path instead."""
self._valid_areas = valid_areas
@classmethod
def from_areas_file_path(cls, areas_file: pathlib.Path) -> "Validator":
"""Build a Validator given a path to
docs/contribute/governance/areas/_areas.yaml."""
with areas_file.open() as f:
parsed_areas = yaml.safe_load(f)
return Validator(area_names_from_file(parsed_areas))
def detect_violations(
self, category: Optional[str], atoms: Sequence[Atom]
) -> Iterator[str]:
"""Yield strings describing all violations found in `atoms`."""
yield from detect_collisions(atoms)
if category:
yield from detect_category_violations(category, atoms)
yield from self.detect_area_violations(atoms)
def detect_area_violations(self, atoms: Sequence[Atom]) -> Iterator[str]:
"""Yields strings describing any invalid API areas in `atoms`."""
for atom in atoms:
if (
atom.area is None
and atom.category in _AREA_REQUIRED_CATEGORIES
and atom.type not in _AREA_OPTIONAL_TYPES
):
yield (
"%s must specify an API area. Valid areas: %s"
% (
atom,
self._valid_areas,
)
)
if atom.area is not None and atom.area not in self._valid_areas:
if matches := difflib.get_close_matches(
atom.area, self._valid_areas
):
yield (
"%s specifies invalid API area '%s'. Did you mean one of these? %s"
% (atom, atom.area, matches)
)
else:
yield (
"%s specifies invalid API area '%s'. Valid areas: %s"
% (atom, atom.area, self._valid_areas)
)