// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.packages;

import com.google.auto.value.AutoValue;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
import com.google.devtools.build.lib.vfs.PathFragment;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Starlark;

/**
 * Represents one of the following:
 *
 * <ul>
 *   <li>A single package (e.g. "//foo/bar")
 *   <li>All transitive subpackages of a package, inclusive (e.g. "//foo/bar/...", which includes
 *       "//foo/bar")
 *   <li>All packages (i.e. "//...")
 * </ul>
 *
 * <p>Typically (exclusively?) used for package visibility, as part of a {@link PackageGroup}
 * target.
 *
 * <p>A package specification is specific to a single {@link RepositoryName} unless it is the "all
 * packages" specification.
 */
// TODO(b/279784354): Delete PackageSpecification; reimplement bzl visibility using something else.
public abstract class PackageSpecification {
  private static final String PUBLIC_VISIBILITY = "public";
  private static final String PRIVATE_VISIBILITY = "private";
  private static final String ALL_BENEATH_SUFFIX = "/...";
  private static final String NEGATIVE_PREFIX = "-";

  // Used for interpreting `visibility` labels.
  private static final String PACKAGE_LABEL = "__pkg__";
  private static final String SUBTREE_LABEL = "__subpackages__";

  /** Returns {@code true} if the package spec includes the provided {@code packageName}. */
  protected abstract boolean containsPackage(PackageIdentifier packageName);

  /**
   * Returns a string representation of this package spec.
   *
   * <p>The repository is included, unless it is the main repository, in which case there will be no
   * leading {@literal @}. For instance, {@code "@somerepo//pkg/subpkg"} and {@code
   * "//otherpkg/..."} are both valid outputs.
   *
   * <p>Note that since {@link #fromString} does not accept label strings with repositories, this
   * representation is not guaranteed to be round-trippable.
   *
   * <p>If {@code includeDoubleSlash} is false, then in the case of the main repository, the leading
   * {@code //} will also be omitted, so that the output looks like {@code otherpkg/...}. This form
   * is deprecated.
   */
  // TODO(b/77598306): Remove the parameter after switching all callers to pass true.
  protected abstract String asString(boolean includeDoubleSlash);

  @Override
  public String toString() {
    return asString(/*includeDoubleSlash=*/ false);
  }

  /**
   * Parses the string {@code spec} into a {@link PackageSpecification}, within the context of the
   * given repository name.
   *
   * <p>{@code spec} may have the following forms:
   *
   * <ol>
   *   <li>The full name of a single package, without repository qualification, prefixed with "//"
   *       (e.g. "//foo/bar"). The resulting specification contains exactly that package.
   *   <li>The same, but suffixed with "/..." for a non-root package ("//foo/bar/...") or "..." for
   *       the root package ("//..."). The resulting specification contains that package and all its
   *       subpackages.
   *   <li>The string constants "public" or "private". The resulting specification contains either
   *       all packages or no packages, respectively.
   * </ol>
   *
   * In the first two cases, the repository of the given package name is taken to be {@code
   * repositoryName}. In the third case the repository name is ignored.
   *
   * <p>In the first two cases, {@code spec} may also be prefixed by a "-". The resulting
   * specification contains the same set of packages but is marked as being negated. (Negation logic
   * is applied at the level of {@link PackageGroupContents}.)
   *
   * <p>Setting {@code allowPublicPrivate} to false disallows the string constants "public" and
   * "private". Note that if {@link #asString} is called with {@code includeDoubleSlash} set to
   * false, the stringification of "public" and "//public" is ambiguous (likewise for private),
   * hence why it might be appropriate to prohibit these forms.
   *
   * <p>Setting {@code repoRootMeansCurrentRepo} to false restores the following legacy behavior: In
   * the specific case where {@code spec} is "//..." (or its negation), the package specification
   * contains <i>all</i> packages (possibly marked as negated) rather than just those packages in
   * {@code repositoryName}. In other words, "//..." behaves the same as "public". However, "//"
   * still represents just the root package of {@code repositoryName}.
   *
   * <p>To protect against requiring users to update to a disallowed syntax, it is illegal to
   * specify {@code repoRootMeansCurrentRepo} without also specifying {@code allowPublicPrivate}.
   *
   * @throws InvalidPackageSpecificationException if the string does not fit one of these forms
   */
  // TODO(#16365): Remove allowPublicPrivate.
  // TODO(#16324): Remove legacy behavior and repoRootMeansCurrentRepo param.
  public static PackageSpecification fromString(
      RepositoryName repositoryName,
      String spec,
      boolean allowPublicPrivate,
      boolean repoRootMeansCurrentRepo)
      throws InvalidPackageSpecificationException {
    if (repoRootMeansCurrentRepo && !allowPublicPrivate) {
      throw new InvalidPackageSpecificationException(
          "Cannot use new \"//...\" meaning without allowing new \"public\" syntax. Try enabling"
              + " --incompatible_package_group_has_public_syntax or disabling"
              + " --incompatible_fix_package_group_reporoot_syntax.");
    }
    if (!allowPublicPrivate
        && (spec.equals(PUBLIC_VISIBILITY) || spec.equals(PRIVATE_VISIBILITY))) {
      throw new InvalidPackageSpecificationException(
          String.format(
              "Use of \"%s\" package specification requires enabling"
                  + " --incompatible_package_group_has_public_syntax",
              spec));
    }
    boolean negative = false;
    if (spec.startsWith(NEGATIVE_PREFIX)) {
      negative = true;
      spec = spec.substring(NEGATIVE_PREFIX.length());
      if (spec.equals(PUBLIC_VISIBILITY) || spec.equals(PRIVATE_VISIBILITY)) {
        throw new InvalidPackageSpecificationException(
            String.format("Cannot negate \"%s\" package specification", spec));
      }
    }
    PackageSpecification packageSpecification =
        fromStringPositive(repositoryName, spec, repoRootMeansCurrentRepo);
    return negative ? new NegativePackageSpecification(packageSpecification) : packageSpecification;
  }

  private static PackageSpecification fromStringPositive(
      RepositoryName repositoryName, String spec, boolean repoRootMeansCurrentRepo)
      throws InvalidPackageSpecificationException {
    if (spec.equals(PUBLIC_VISIBILITY)) {
      return AllPackages.INSTANCE;
    } else if (spec.equals(PRIVATE_VISIBILITY)) {
      return NoPackages.INSTANCE;
    }
    if (!spec.startsWith("//")) {
      throw new InvalidPackageSpecificationException(
          String.format(
              "invalid package name '%s': must start with '//' or be 'public' or 'private'", spec));
    }

    String pkgPath;
    boolean allBeneath = false;
    if (spec.endsWith(ALL_BENEATH_SUFFIX)) {
      allBeneath = true;
      pkgPath = spec.substring(0, spec.length() - ALL_BENEATH_SUFFIX.length());
      if (pkgPath.equals("/")) {
        // spec was "//...".
        if (repoRootMeansCurrentRepo) {
          pkgPath = "//";
        } else {
          // Legacy behavior: //... is "public".
          return AllPackages.INSTANCE;
        }
      }
    } else {
      pkgPath = spec;
    }

    PackageIdentifier unqualifiedPkgId;
    try {
      unqualifiedPkgId = PackageIdentifier.parse(pkgPath);
    } catch (LabelSyntaxException e) {
      throw new InvalidPackageSpecificationException(
          String.format("invalid package name '%s': %s", spec, e.getMessage()));
    }
    Verify.verify(unqualifiedPkgId.getRepository().isMain());

    PackageIdentifier pkgId =
        PackageIdentifier.create(repositoryName, unqualifiedPkgId.getPackageFragment());
    return allBeneath ? new AllPackagesBeneath(pkgId) : new SinglePackage(pkgId);
  }

  /**
   * Parses a string to a {@code PackageSpecification} for use with .bzl load visibility.
   *
   * <p>This rejects negative package patterns, and translates the exception type into {@code
   * EvalException}.
   *
   * <p>Note that load visibility package specifications always behave as if {@code
   * --incompatible_package_group_has_public_syntax} and {@code
   * --incompatible_fix_package_group_reporoot_syntax} are enabled.
   */
  public static PackageSpecification fromStringForBzlVisibility(
      RepositoryName repositoryName, String spec) throws EvalException {
    PackageSpecification result;
    try {
      result =
          fromString(
              repositoryName,
              spec,
              /*allowPublicPrivate=*/ true,
              /*repoRootMeansCurrentRepo=*/ true);
    } catch (InvalidPackageSpecificationException e) {
      throw new EvalException(e.getMessage());
    }
    if (result instanceof NegativePackageSpecification) {
      throw Starlark.errorf("Cannot use negative package patterns here");
    }
    return result;
  }

  /**
   * Parses the provided {@link Label} into a {@link PackageSpecification} specific to the {@link
   * RepositoryName} associated with the label.
   *
   * <p>If {@code label.getName.equals("__pkg__")} then this results in a {@link
   * PackageSpecification} that contains exactly the named package.
   *
   * <p>If {@code label.getName.equals("__subpackages__")} then this results in a {@link
   * PackageSpecification} that contains all transitive subpackages of that package, inclusive.
   *
   * <p>If the label's name is neither "__pkg__" nor "__subpackages__", this returns {@code null}.
   *
   * <p>Note that there is no {@link Label} associated with the {@link RepositoryName}-agnostic
   * "public" specification ("//..." under legacy semantics).
   */
  @Nullable
  static PackageSpecification fromLabel(Label label) {
    if (label.getName().equals(PACKAGE_LABEL)) {
      return new SinglePackage(label.getPackageIdentifier());
    } else if (label.getName().equals(SUBTREE_LABEL)) {
      return new AllPackagesBeneath(label.getPackageIdentifier());
    } else {
      return null;
    }
  }

  public static PackageSpecification everything() {
    return AllPackages.INSTANCE;
  }

  public static PackageSpecification nothing() {
    return NoPackages.INSTANCE;
  }

  private static final class SinglePackage extends PackageSpecification {
    private final PackageIdentifier singlePackageName;

    SinglePackage(PackageIdentifier singlePackageName) {
      this.singlePackageName = singlePackageName;
    }

    @Override
    protected boolean containsPackage(PackageIdentifier packageName) {
      return this.singlePackageName.equals(packageName);
    }

    @Override
    protected String asString(boolean includeDoubleSlash) {
      return PackageGroupContents.stringForSinglePackage(singlePackageName, includeDoubleSlash);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof SinglePackage)) {
        return false;
      }
      SinglePackage that = (SinglePackage) o;
      return singlePackageName.equals(that.singlePackageName);
    }

    @Override
    public int hashCode() {
      return singlePackageName.hashCode();
    }
  }

  private static final class AllPackagesBeneath extends PackageSpecification {
    private final PackageIdentifier prefix;

    AllPackagesBeneath(PackageIdentifier prefix) {
      this.prefix = prefix;
    }

    @Override
    protected boolean containsPackage(PackageIdentifier packageName) {
      return packageName.getRepository().equals(prefix.getRepository())
          && packageName.getPackageFragment().startsWith(prefix.getPackageFragment());
    }

    @Override
    protected String asString(boolean includeDoubleSlash) {
      return PackageGroupContents.stringForAllPackagesBeneath(prefix, includeDoubleSlash);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof AllPackagesBeneath)) {
        return false;
      }
      AllPackagesBeneath that = (AllPackagesBeneath) o;
      return prefix.equals(that.prefix);
    }

    @Override
    public int hashCode() {
      return prefix.hashCode();
    }
  }

  /** A package specification for a negative match, e.g. {@code -//pkg/sub/...}. */
  private static final class NegativePackageSpecification extends PackageSpecification {
    private final PackageSpecification delegate;

    NegativePackageSpecification(PackageSpecification delegate) {
      this.delegate = delegate;
    }

    @Override
    protected boolean containsPackage(PackageIdentifier packageName) {
      return delegate.containsPackage(packageName);
    }

    @Override
    protected String asString(boolean includeDoubleSlash) {
      return "-" + delegate.asString(includeDoubleSlash);
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      return obj instanceof NegativePackageSpecification
          && delegate.equals(((NegativePackageSpecification) obj).delegate);
    }

    @Override
    public int hashCode() {
      return NegativePackageSpecification.class.hashCode() ^ delegate.hashCode();
    }
  }

  @VisibleForSerialization
  static final class AllPackages extends PackageSpecification {
    @SerializationConstant @VisibleForSerialization
    static final PackageSpecification INSTANCE = new AllPackages();

    @Override
    protected boolean containsPackage(PackageIdentifier packageName) {
      return true;
    }

    @Override
    protected String asString(boolean includeDoubleSlash) {
      return PackageGroupContents.stringForAllPackages(includeDoubleSlash);
    }

    @Override
    public boolean equals(Object o) {
      return o instanceof AllPackages;
    }

    @Override
    public int hashCode() {
      return AllPackages.class.hashCode();
    }
  }

  @VisibleForSerialization
  static final class NoPackages extends PackageSpecification {
    @SerializationConstant @VisibleForSerialization
    static final PackageSpecification INSTANCE = new NoPackages();

    @Override
    protected boolean containsPackage(PackageIdentifier packageName) {
      return false;
    }

    @Override
    protected String asString(boolean includeDoubleSlash) {
      return "private";
    }

    @Override
    public boolean equals(Object o) {
      return o instanceof NoPackages;
    }

    @Override
    public int hashCode() {
      return NoPackages.class.hashCode();
    }
  }

  /** Exception class to be thrown when a specification cannot be parsed. */
  static class InvalidPackageSpecificationException extends Exception {
    private InvalidPackageSpecificationException(String message) {
      super(message);
    }
  }

  /**
   * Represents a collection of {@link PackageSpecification}s logically corresponding to a single
   * {@code package_group}'s {@code packages} attribute.
   *
   * <p>Supports testing whether a given package is contained, taking into account negative specs.
   *
   * <p>Duplicate specs (e.g., ["//foo", "//foo"]) may or may not be deduplicated. Iteration order
   * may vary from the order in which specs were provided, but is guaranteed to be deterministic.
   *
   * <p>For modeling a {@code package_group}'s transitive contents (i.e., via the {@code includes}
   * attribute), see {@link PackageSpecificationProvider}.
   */
  @AutoValue
  public abstract static class PackageGroupContents {
    // This class is optimized for memory and cpu.
    // TODO(b/279784354): Further improvements are possible.
    //
    // PackageGroupContents instances are retained through and after the analysis phase. Therefore,
    // in order to save memory, we don't retain PackageSpecification instances but instead unroll
    // them, storing just lists and sets of PackageIdentifier instances.
    //
    // To save cpu, we store the PackageIdentifier instances corresponding to positive/negative
    // single package specifications in sets so we get a fast-path hit on them. Also, we check
    // negatives first since package_group semantics require we check all negatives no matter what.
    // Both of these cpu optimizations combine well in practice since there are some package_group
    // targets with very large lists of negative single package specifications.

    abstract ImmutableSet<PackageIdentifier> singlePackagePositives();

    abstract ImmutableList<PackageIdentifier> allPackagesBeneathPositives();

    abstract boolean hasPositiveAllPackages();

    abstract ImmutableSet<PackageIdentifier> singlePackageNegatives();

    abstract ImmutableList<PackageIdentifier> allPackagesBeneathNegatives();

    abstract boolean hasPrivate();

    abstract boolean hasNegativeAllPackages();

    /**
     * Creates a {@link PackageGroupContents} representing a collection of {@link
     * PackageSpecification}s.
     */
    public static PackageGroupContents create(
        ImmutableList<PackageSpecification> packageSpecifications) {
      var singlePackagePositivesBuilder = ImmutableSet.<PackageIdentifier>builder();
      var allPackagesBeneathPositivesBuilder = ImmutableList.<PackageIdentifier>builder();
      boolean hasPositiveAllPackages = false;
      var singlePackageNegativesBuilder = ImmutableSet.<PackageIdentifier>builder();
      var allPackagesBeneathNegativesBuilder = ImmutableList.<PackageIdentifier>builder();
      boolean hasPrivate = false;
      boolean hasNegativeAllPackages = false;

      for (PackageSpecification spec : packageSpecifications) {
        if (spec instanceof SinglePackage) {
          singlePackagePositivesBuilder.add(((SinglePackage) spec).singlePackageName);
          continue;
        }

        if (spec instanceof AllPackagesBeneath) {
          allPackagesBeneathPositivesBuilder.add(((AllPackagesBeneath) spec).prefix);
          continue;
        }

        if (spec instanceof AllPackages) {
          hasPositiveAllPackages = true;
          continue;
        }

        if (spec instanceof NoPackages) {
          // We can't drop NoPackages because it still needs to be serialized, e.g. in bazel query
          // output.
          hasPrivate = true;
          continue;
        }

        PackageSpecification delegate = ((NegativePackageSpecification) spec).delegate;
        if (delegate instanceof SinglePackage) {
          singlePackageNegativesBuilder.add(((SinglePackage) delegate).singlePackageName);
          continue;
        }

        if (delegate instanceof AllPackagesBeneath) {
          allPackagesBeneathNegativesBuilder.add(((AllPackagesBeneath) delegate).prefix);
          continue;
        }

        if (delegate instanceof AllPackages) {
          hasNegativeAllPackages = true;
          continue;
        }

        throw new IllegalStateException(spec.toString());
      }

      return new AutoValue_PackageSpecification_PackageGroupContents(
          singlePackagePositivesBuilder.build(),
          allPackagesBeneathPositivesBuilder.build(),
          hasPositiveAllPackages,
          singlePackageNegativesBuilder.build(),
          allPackagesBeneathNegativesBuilder.build(),
          hasPrivate,
          hasNegativeAllPackages);
    }

    /**
     * Returns true if the given package matches at least one of this {@code PackageGroupContents}'
     * positive specifications and none of its negative specifications.
     */
    public final boolean containsPackage(PackageIdentifier packageIdentifier) {
      // DO NOT use streams or iterators here as they create excessive garbage.
      if (hasNegativeAllPackages()) {
        return false;
      }
      if (singlePackageNegatives().contains(packageIdentifier)) {
        return false;
      }
      // The following line is just so that we don't call the method inside the loop (which may or
      // may not be optimized away... better be on the safe side).
      var allPackagesBeneathNegatives = allPackagesBeneathNegatives();
      for (int i = 0; i < allPackagesBeneathNegatives.size(); i++) {
        if (matchesAllPackagesBeneath(packageIdentifier, allPackagesBeneathNegatives.get(i))) {
          return false;
        }
      }
      if (hasPositiveAllPackages()) {
        return true;
      }
      if (singlePackagePositives().contains(packageIdentifier)) {
        return true;
      }
      var allPackagesBeneathPositives = allPackagesBeneathPositives();
      for (int i = 0; i < allPackagesBeneathPositives.size(); i++) {
        if (matchesAllPackagesBeneath(packageIdentifier, allPackagesBeneathPositives.get(i))) {
          return true;
        }
      }
      return false;
    }

    private static boolean matchesAllPackagesBeneath(
        PackageIdentifier pkgId, PackageIdentifier prefix) {
      return pkgId.getRepository().equals(prefix.getRepository())
          && pkgId.getPackageFragment().startsWith(prefix.getPackageFragment());
    }

    /**
     * Does the equivalent of mapping {@link PackageSpecification#asString} to the component package
     * specs.
     *
     * <p>Note that strings for specs that cross repositories can't be reparsed using {@link
     * PackageSpecification#fromString}.
     *
     * <p>The special public constant will serialize as {@code "public"} if {@code
     * includeDoubleSlash} is true, and {@code "//..."} otherwise. The private constant will always
     * serialize as {@code "private"},
     */
    public final ImmutableList<String> packageStrings(boolean includeDoubleSlash) {
      ImmutableList.Builder<String> resultBuilder = ImmutableList.builder();
      for (PackageIdentifier pkgId : singlePackagePositives()) {
        resultBuilder.add(stringForSinglePackage(pkgId, includeDoubleSlash));
      }
      for (PackageIdentifier pkgId : allPackagesBeneathPositives()) {
        resultBuilder.add(stringForAllPackagesBeneath(pkgId, includeDoubleSlash));
      }
      for (PackageIdentifier pkgId : singlePackageNegatives()) {
        resultBuilder.add("-" + stringForSinglePackage(pkgId, includeDoubleSlash));
      }
      for (PackageIdentifier pkgId : allPackagesBeneathNegatives()) {
        resultBuilder.add("-" + stringForAllPackagesBeneath(pkgId, includeDoubleSlash));
      }
      if (hasPositiveAllPackages()) {
        resultBuilder.add(stringForAllPackages(includeDoubleSlash));
      }
      if (hasPrivate()) {
        resultBuilder.add("private");
      }
      if (hasNegativeAllPackages()) {
        resultBuilder.add("-" + stringForAllPackages(includeDoubleSlash));
      }
      return resultBuilder.build();
    }

    private static String stringForAllPackages(boolean includeDoubleSlash) {
      // Under legacy formatting rules, use legacy syntax. This avoids ambiguity between "public"
      // and "//public", and ensures that AllPackages is round-trippable when the value of
      // includeDoubleSlash matches allowPublicPrivate.
      return includeDoubleSlash ? "public" : "//...";
    }

    private static String stringForSinglePackage(
        PackageIdentifier pkgId, boolean includeDoubleSlash) {
      if (includeDoubleSlash) {
        return pkgId.getCanonicalForm();
      } else {
        // PackageIdentifier#toString implements the legacy behavior of omitting the double slash
        // for the main repo.
        return pkgId.toString();
      }
    }

    private static String stringForAllPackagesBeneath(
        PackageIdentifier pkgId, boolean includeDoubleSlash) {
      if (pkgId.getPackageFragment().equals(PathFragment.EMPTY_FRAGMENT)) {
        // Special case: Emit "//..." rather than suffixing "/...", which would yield "/...".
        // Make sure not to strip the repo in the case of "@repo//...".
        //
        // Note that "//..." is the desired result, not "...", even under the legacy behavior of
        // includeDoubleSlash=false.
        return pkgId.getCanonicalForm() + "...";
      }
      if (includeDoubleSlash) {
        return pkgId.getCanonicalForm() + ALL_BENEATH_SUFFIX;
      } else {
        // PackageIdentifier#toString implements the legacy behavior of omitting the double slash
        // for the main repo.
        return pkgId.toString() + ALL_BENEATH_SUFFIX;
      }
    }

    /**
     * /** Returns a string representation of this package spec without the repository, and which is
     * round-trippable through {@link #fromString}.
     *
     * <p>For instance, {@code @somerepo//pkg/subpkg/...} turns into {@code "//pkg/subpkg/..."}.
     *
     * <p>Omitting the repository means that the returned strings are ambiguous in the absence of
     * additional context. But, for instance, if interpreted with respect to a {@code
     * package_group}'s {@code packages} attribute, the strings always have the same repository as
     * the package group.
     *
     * <p>Note that this is ambiguous w.r.t. specs that reference other repositories.
     *
     * <p>The special public and private constants will serialize as {@code "public"} and {@code
     * "private"} respectively.
     */
    public final ImmutableList<String> packageStringsWithDoubleSlashAndWithoutRepository() {
      ImmutableList.Builder<String> resultBuilder = ImmutableList.builder();
      for (PackageIdentifier pkgId : singlePackagePositives()) {
        resultBuilder.add(stringForSinglePackageWithDoubleSlashAndWithoutRepository(pkgId));
      }
      for (PackageIdentifier pkgId : allPackagesBeneathPositives()) {
        resultBuilder.add(stringForAllPackagesBeneathWithDoubleSlashAndWithoutRepository(pkgId));
      }
      for (PackageIdentifier pkgId : singlePackageNegatives()) {
        resultBuilder.add("-" + stringForSinglePackageWithDoubleSlashAndWithoutRepository(pkgId));
      }
      for (PackageIdentifier pkgId : allPackagesBeneathNegatives()) {
        resultBuilder.add(
            "-" + stringForAllPackagesBeneathWithDoubleSlashAndWithoutRepository(pkgId));
      }
      if (hasPositiveAllPackages()) {
        resultBuilder.add("public");
      }
      if (hasPrivate()) {
        resultBuilder.add("private");
      }
      return resultBuilder.build();
    }

    private static String stringForSinglePackageWithDoubleSlashAndWithoutRepository(
        PackageIdentifier pkgId) {
      return "//" + pkgId.getPackageFragment().getPathString();
    }

    private static String stringForAllPackagesBeneathWithDoubleSlashAndWithoutRepository(
        PackageIdentifier pkgId) {
      PathFragment pathFragment = pkgId.getPackageFragment();
      return pathFragment.equals(PathFragment.EMPTY_FRAGMENT)
          ? "//..."
          : "//" + pathFragment.getPathString() + ALL_BENEATH_SUFFIX;
    }
  }
}
