chromium/tools/traffic_annotation/scripts/auditor/error.py

# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from pathlib import Path
from enum import Enum, auto
from typing import Optional


class ErrorType(Enum):
  # Annotation syntax is not right.
  SYNTAX = auto()
  # Can't create a MutableNetworkTrafficAnnotationTag from anywhere (except
  # in whitelisted files).
  MUTABLE_TAG = auto()
  # Annotation has some missing fields.
  INCOMPLETE_ANNOTATION = auto()
  # A partial of (branched-)completing annotation is not paired with another
  # annotation to be completed.
  INCOMPLETED_ANNOTATION = auto()
  # Annotation has some inconsistent fields.
  INCONSISTENT_ANNOTATION = auto()
  # Two annotations that are supposed to merge cannot merge.
  MERGE_FAILED = auto()
  # A function is called with the "missing" tag.
  MISSING_TAG_USED = auto()
  # A function is called with the "test" or "test_partial" tag outside of a
  # test file.
  TEST_ANNOTATION = auto()
  # A function is called with NO_TRAFFIC_ANNOTATION_YET tag.
  NO_ANNOTATION = auto()
  # An id has a hash code equal to a reserved word.
  RESERVED_ID_HASH_CODE = auto()
  # An id contains an invalid character (not alphanumeric or underscore).
  ID_INVALID_CHARACTER = auto()
  # An id is used in two places without matching conditions. Proper conditions
  # include when 2 annotations are completing each other, or are different
  # branches of the same annotation.
  REPEATED_ID = auto()
  # Annotation does not have a valid second id.
  MISSING_SECOND_ID = auto()
  # Two ids have equal hash codes.
  HASH_CODE_COLLISION = auto()
  # "os_list" is invalid in annotations.xml.
  INVALID_OS = auto()
  # "added_in_milestone" is invalid in annotations.xml.
  INVALID_ADDED_IN = auto()
  # annotations.xml requires an update.
  ANNOTATIONS_XML_UPDATE = auto()
  # grouping.xml requires an update.
  GROUPING_XML_UPDATE = auto()
  # Annotations should be added to grouping.xml.
  ADD_GROUPING_XML = auto()
  # Annotations should be removed from grouping.xml.
  REMOVE_GROUPING_XML = auto()
  # Annotation is missing internal email, user_data
  # or last_reviewed fields
  MISSING_NEW_FIELDS = auto()
  # Annotation should be removed from safe_list.txt
  REMOVE_FROM_SAFE_LIST = auto()
  # User data type should not be unspecified
  INVALID_USER_DATA_TYPE = auto()
  # Date format should be YYYY-mm-dd
  INVALID_DATE_FORMAT = auto()


class AuditorError:
  def __init__(self,
               result_type: ErrorType,
               message: str = "",
               file_path: Optional[Path] = None,
               line: int = 0,
               *extra_details: str):
    self.type = result_type
    self.message = message
    self.file_path = file_path
    self.line = line
    self._details = []

    assert message or result_type in [
        ErrorType.MISSING_TAG_USED, ErrorType.TEST_ANNOTATION,
        ErrorType.NO_ANNOTATION, ErrorType.MISSING_SECOND_ID,
        ErrorType.MUTABLE_TAG, ErrorType.INVALID_OS, ErrorType.INVALID_ADDED_IN,
        ErrorType.INVALID_DATE_FORMAT
    ]

    if message:
      self._details.append(message)
    self._details.extend(extra_details)

  def __str__(self) -> str:
    if self.type == ErrorType.SYNTAX:
      assert self._details
      return ("SYNTAX: Annotation at '{}:{}' has the following syntax"
              " error: {}".format(self.file_path, self.line,
                                  str(self._details[0]).replace("\n", " ")))

    if self.type == ErrorType.MUTABLE_TAG:
      return ("MUTABLE_TAG: Calling CreateMutableNetworkTrafficAnnotationTag() "
              "is not safelisted at '{}:{}'.".format(self.file_path, self.line))

    if self.type == ErrorType.INCOMPLETE_ANNOTATION:
      assert self._details
      return ("INCOMPLETE_ANNOTATION: Annotation at '{}:{}' has the"
              " following missing fields: {}".format(self.file_path, self.line,
                                                     self._details[0]))

    if self.type == ErrorType.INCOMPLETED_ANNOTATION:
      assert self._details
      return ("INCOMPLETED_ANNOTATION: Annotation '{}' is never "
              "completed.".format(self._details[0]))

    if self.type == ErrorType.INCONSISTENT_ANNOTATION:
      assert self._details
      return ("INCONSISTENT_ANNOTATION: Annotation at '{}:{}' has the "
              "following inconsistencies: {}".format(self.file_path, self.line,
                                                     self._details[0]))
    if self.type == ErrorType.MERGE_FAILED:
      assert len(self._details) == 3
      return ("MERGE_FAILED: Annotations '{}' and '{}' cannot be merged due to "
              "the following error(s): {}".format(self._details[1],
                                                  self._details[2],
                                                  self._details[0]))

    if self.type == ErrorType.MISSING_TAG_USED:
      return ("MISSING_TAG_USED: MISSING_TRAFFIC_ANNOTATION tag used in "
              "'{}:{}'.".format(self.file_path, self.line))

    if self.type == ErrorType.TEST_ANNOTATION:
      return ("TEST_ANNOTATION: Annotation for tests used in '{}:{}'.".format(
          self.file_path, self.line))

    if self.type == ErrorType.NO_ANNOTATION:
      return "NO_ANNOTATION: Empty annotation in '{}:{}'.".format(
          self.file_path, self.line)

    if self.type == ErrorType.RESERVED_ID_HASH_CODE:
      assert self._details
      return ("RESERVED_ID_HASH_CODE: Id '{}' in '{}:{}' has a hash code equal "
              "to a reserved word and should be changed.".format(
                  self._details[0], self.file_path, self.line))

    if self.type == ErrorType.HASH_CODE_COLLISION:
      assert len(self._details) == 2
      return ("HASH_CODE_COLLISION: The following annotations have colliding "
              "hash codes and should be updated: '{}', '{}'.".format(
                  self._details[0], self._details[1]))

    if self.type == ErrorType.REPEATED_ID:
      assert len(self._details) == 2
      return ("REPEATED_ID: The following annotations have equal ids and "
              "should be updated: {}, {}.".format(self._details[0],
                                                  self._details[1]))

    if self.type == ErrorType.ID_INVALID_CHARACTER:
      assert self._details
      return ("ID_INVALID_CHARACTER: Id '{}' in '{}:{}' contains an invalid "
              "character.".format(self._details[0], self.file_path, self.line))

    if self.type == ErrorType.MISSING_SECOND_ID:
      return ("MISSING_SECOND_ID: Second id of annotation at '{}:{}' should be "
              "updated, as it has the same hash code as the first one.".format(
                  self.file_path, self.line))

    if self.type == ErrorType.INVALID_OS:
      assert len(self._details) == 2
      return ("INVALID_OS: Invalid OS '{}' in annotation '{}' at {}.".format(
          self._details[0], self._details[1], self.file_path))

    if self.type == ErrorType.INVALID_ADDED_IN:
      assert len(self._details) == 2
      return ("INVALID_ADDED_IN: Invalid or missing added_in_milestone '{}' in "
              "annotation '{}' at {}.".format(self._details[0],
                                              self._details[1], self.file_path))

    if self.type == ErrorType.ADD_GROUPING_XML:
      assert self._details
      return ("ADD_GROUPING_XML: The following annotations should be added "
              "to an existing group in "
              "tools/traffic_annotation/summary/grouping.xml: {}.".format(
                  self._details[0]))

    if self.type == ErrorType.REMOVE_GROUPING_XML:
      assert self._details
      return ("REMOVE_GROUPING_XML: The following annotations are not needed "
              "in tools/traffic_annotation/summary/grouping.xml, and should be "
              "removed: {}.".format(self._details[0]))

    if self.type == ErrorType.ANNOTATIONS_XML_UPDATE:
      assert self._details
      return (
          "'tools/traffic_annotation/summary/annotations.xml' requires update. "
          "It is recommended to run the Traffic Annotation Auditor locally to "
          "do the updates automatically (please refer to tools/"
          "traffic_annotation/scripts/auditor/README.md), but you can also "
          "apply the following edit(s) to do it manually:\n{}".format(
              self._details[0]))

    if self.type == ErrorType.GROUPING_XML_UPDATE:
      assert self._details
      return (
          "'tools/traffic_annotation/summary/grouping.xml' requires update. "
          "It is recommended to run the Traffic Annotation Auditor locally to "
          "do the updates automatically (please refer to tools/"
          "traffic_annotation/scripts/auditor/README.md), but you can also "
          "apply the following edit(s) to do it manually:\n{}".format(
              self._details[0]))

    if self.type == ErrorType.MISSING_NEW_FIELDS:
      assert self._details
      return ("MISSING_NEW_FIELDS: Annotation at '{}:{}' {}".format(
          self.file_path, self.line, self._details[0]))
    if self.type == ErrorType.REMOVE_FROM_SAFE_LIST:
      assert self._details
      return ("REMOVE_FROM_SAFE_LIST: {}. Remove {} from safe_list.txt".format(
          self._details[0], self.file_path))

    if self.type == ErrorType.INVALID_USER_DATA_TYPE:
      assert self._details
      return (
          "Invalid value of user_data::type: {} in annotation at {}:{}".format(
              self._details[0], self.file_path, self.line))

    if self.type == ErrorType.INVALID_DATE_FORMAT:
      assert self._details
      return ("Date format should be {} in annotation at {}:{}".format(
          self._details[0], self.file_path, self.line))

    raise NotImplementedError("Unimplemented ErrorType: {}".format(
        self.type.name))

  def __repr__(self) -> str:
    return "AuditorError(\"{}\")".format(str(self))