Skip to content

Commit 86f734d

Browse files
brandjoncopybara-github
authored andcommitted
Clean up PackageSpecification stringification
This CL gives us a pathway for fixing stringification of package_group's `packages` attribute, so that //foo/bar is printed as "//foo/bar" and not "foo/bar". This affects e.g. `bazel query --output=proto [...]`. This change is a prerequisite to adding support for "public" and "private" string constant package specifiers, since otherwise "public" would be ambiguous with the stringification of //public. PackageSpecification currently has two forms of stringification. The toString() method is unambiguous but omits the leading double slash, while the toStringWithoutRepository() always includes the double slash but never includes the repo name. I factored toString() into asString(boolean includeDoubleSlash) to switch between the old form with and without the fix to include slashes, and made toString() and all current callers pass false for now. I just renamed toStringWithoutRepository to asStringWithoutRepository for symmetry. Both are now clearly documented. In PackageGroupContents, I clarified its behavior w.r.t. duplicates and order, and renamed its member vars for simplicity and in anticipation of storing another type of package spec subclass in a follow-up CL. Also renamed some methods for clarity. Fixed a bug in AllPackagesBeneath where //... parsed in another repo would stringify without the leading "@repo". However, this bug was never exercised since it would require the follow-up CL's behavior of enabling "//..." to parse as AllPackagesBeneath rather than AllPackages (#16323). Added a test for all three forms of stringification to PackageGroupTest. There should be no actual behavioral change enabled in this CL. Work toward #11261. PiperOrigin-RevId: 478828269 Change-Id: Ifc7c08e6e90b89d33bed1ee6a5a97b3a3ac07326
1 parent a665f8f commit 86f734d

File tree

5 files changed

+219
-95
lines changed

5 files changed

+219
-95
lines changed

src/main/java/com/google/devtools/build/lib/packages/PackageGroup.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,11 @@ public List<Label> getIncludes() {
9292
return includes;
9393
}
9494

95-
public List<String> getContainedPackages() {
96-
return packageSpecifications.containedPackages().collect(toImmutableList());
95+
// See PackageSpecification#asString.
96+
public List<String> getContainedPackages(boolean includeDoubleSlash) {
97+
return packageSpecifications
98+
.streamPackageStrings(includeDoubleSlash)
99+
.collect(toImmutableList());
97100
}
98101

99102
@Override

src/main/java/com/google/devtools/build/lib/packages/PackageSpecification.java

Lines changed: 142 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.google.common.base.Verify;
1717
import com.google.common.collect.ImmutableList;
1818
import com.google.common.collect.ImmutableMap;
19+
import com.google.common.collect.Streams;
1920
import com.google.devtools.build.lib.cmdline.Label;
2021
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
2122
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
@@ -24,7 +25,6 @@
2425
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
2526
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
2627
import com.google.devtools.build.lib.vfs.PathFragment;
27-
import java.util.LinkedHashMap;
2828
import java.util.stream.Stream;
2929
import javax.annotation.Nullable;
3030
import net.starlark.java.eval.EvalException;
@@ -56,13 +56,41 @@ public abstract class PackageSpecification {
5656
protected abstract boolean containsPackage(PackageIdentifier packageName);
5757

5858
/**
59-
* Returns a {@link String} representation of the {@link PackageSpecification} of the same format
60-
* accepted by {@link #fromString}.
59+
* Returns a string representation of this package spec.
6160
*
62-
* <p>The returned {@link String} is insensitive to the {@link RepositoryName} associated with the
63-
* {@link PackageSpecification}.
61+
* <p>The repository is included, unless it is the main repository, in which case there will be no
62+
* leading {@literal @}. For instance, {@code "@somerepo//pkg/subpkg"} and {@code
63+
* "//otherpkg/..."} are both valid outputs.
64+
*
65+
* <p>Note that since {@link #fromString} does not accept label strings with repositories, this
66+
* representation is not guaranteed to be round-trippable.
67+
*
68+
* <p>If {@code includeDoubleSlash} is false, then in the case of the main repository, the leading
69+
* {@code //} will also be omitted, so that the output looks like {@code otherpkg/...}. This form
70+
* is deprecated.
71+
*/
72+
// TODO(b/77598306): Remove the parameter after switching all callers to pass true.
73+
protected abstract String asString(boolean includeDoubleSlash);
74+
75+
/**
76+
* Returns a string representation of this package spec without the repository, and which is
77+
* round-trippable through {@link #fromString}.
78+
*
79+
* <p>For instance, {@code @somerepo//pkg/subpkg/...} turns into {@code "//pkg/subpkg/..."}.
80+
*
81+
* <p>Omitting the repository means that the returned strings are ambiguous in the absence of
82+
* additional context. But, for instance, if interpreted with respect to a {@code package_group}'s
83+
* {@code packages} attribute, the strings always have the same repository as the package group.
6484
*/
65-
protected abstract String toStringWithoutRepository();
85+
// TODO(brandjon): This method's main benefit is that it's round-trippable. We could eliminate
86+
// it in favor of asString() if we provided a public variant of fromString() that tolerates
87+
// repositories.
88+
protected abstract String asStringWithoutRepository();
89+
90+
@Override
91+
public String toString() {
92+
return asString(/*includeDoubleSlash=*/ false);
93+
}
6694

6795
/**
6896
* Parses the provided {@link String} into a {@link PackageSpecification}.
@@ -196,13 +224,19 @@ protected boolean containsPackage(PackageIdentifier packageName) {
196224
}
197225

198226
@Override
199-
protected String toStringWithoutRepository() {
200-
return "//" + singlePackageName.getPackageFragment().getPathString();
227+
protected String asString(boolean includeDoubleSlash) {
228+
if (includeDoubleSlash) {
229+
return singlePackageName.getCanonicalForm();
230+
} else {
231+
// PackageIdentifier#toString implements the legacy behavior of omitting the double slash
232+
// for the main repo.
233+
return singlePackageName.toString();
234+
}
201235
}
202236

203237
@Override
204-
public String toString() {
205-
return singlePackageName.toString();
238+
protected String asStringWithoutRepository() {
239+
return "//" + singlePackageName.getPackageFragment().getPathString();
206240
}
207241

208242
@Override
@@ -237,16 +271,27 @@ protected boolean containsPackage(PackageIdentifier packageName) {
237271
}
238272

239273
@Override
240-
protected String toStringWithoutRepository() {
241-
return "//" + prefix.getPackageFragment().getPathString() + ALL_BENEATH_SUFFIX;
274+
protected String asString(boolean includeDoubleSlash) {
275+
if (prefix.getPackageFragment().equals(PathFragment.EMPTY_FRAGMENT)) {
276+
// Special case: Emit "//..." rather than suffixing "/...", which would yield "/...".
277+
// Make sure not to strip the repo in the case of "@repo//...".
278+
//
279+
// Note that "//..." is the desired result, not "...", even under the legacy behavior of
280+
// includeDoubleSlash=false.
281+
return prefix.getCanonicalForm() + "...";
282+
}
283+
if (includeDoubleSlash) {
284+
return prefix.getCanonicalForm() + ALL_BENEATH_SUFFIX;
285+
} else {
286+
// PackageIdentifier#toString implements the legacy behavior of omitting the double slash
287+
// for the main repo.
288+
return prefix.toString() + ALL_BENEATH_SUFFIX;
289+
}
242290
}
243291

244292
@Override
245-
public String toString() {
246-
if (prefix.getPackageFragment().equals(PathFragment.EMPTY_FRAGMENT)) {
247-
return "//...";
248-
}
249-
return prefix + "/...";
293+
protected String asStringWithoutRepository() {
294+
return "//" + prefix.getPackageFragment().getPathString() + ALL_BENEATH_SUFFIX;
250295
}
251296

252297
@Override
@@ -281,8 +326,13 @@ protected boolean containsPackage(PackageIdentifier packageName) {
281326
}
282327

283328
@Override
284-
protected String toStringWithoutRepository() {
285-
return "-" + delegate.toStringWithoutRepository();
329+
protected String asString(boolean includeDoubleSlash) {
330+
return "-" + delegate.asString(includeDoubleSlash);
331+
}
332+
333+
@Override
334+
protected String asStringWithoutRepository() {
335+
return "-" + delegate.asStringWithoutRepository();
286336
}
287337

288338
@Override
@@ -298,11 +348,6 @@ public boolean equals(Object obj) {
298348
return obj instanceof NegativePackageSpecification
299349
&& delegate.equals(((NegativePackageSpecification) obj).delegate);
300350
}
301-
302-
@Override
303-
public String toString() {
304-
return "-" + delegate;
305-
}
306351
}
307352

308353
@VisibleForSerialization
@@ -316,7 +361,14 @@ protected boolean containsPackage(PackageIdentifier packageName) {
316361
}
317362

318363
@Override
319-
protected String toStringWithoutRepository() {
364+
protected String asString(boolean includeDoubleSlash) {
365+
// Note that "//..." is the desired result, not "...", even under the legacy behavior of
366+
// includeDoubleSlash=false.
367+
return "//...";
368+
}
369+
370+
@Override
371+
protected String asStringWithoutRepository() {
320372
return "//...";
321373
}
322374

@@ -329,11 +381,6 @@ public boolean equals(Object o) {
329381
public int hashCode() {
330382
return "//...".hashCode();
331383
}
332-
333-
@Override
334-
public String toString() {
335-
return "//...";
336-
}
337384
}
338385

339386
/** Exception class to be thrown when a specification cannot be parsed. */
@@ -344,23 +391,36 @@ private InvalidPackageSpecificationException(String message) {
344391
}
345392

346393
/**
347-
* A collection of {@link PackageSpecification}s from a {@code package_group}, which supports
348-
* testing a given package for containment (see {@link #containedPackages()}}.
394+
* A collection of {@link PackageSpecification}s logically corresponding to a single {@code
395+
* package_group}'s {@code packages} attribute.
396+
*
397+
* <p>Supports testing whether a given package is contained, taking into account negative specs.
398+
*
399+
* <p>Duplicate specs (e.g., ["//foo", "//foo"]) may or may not be deduplicated. Iteration order
400+
* may vary from the order in which specs were provided, but is guaranteed to be deterministic.
401+
*
402+
* <p>For modeling a {@code package_group}'s transitive contents (i.e., via the {@code includes}
403+
* attribute), see {@link PackageSpecificationProvider}.
349404
*/
350405
@Immutable
351406
public static final class PackageGroupContents {
352-
private final ImmutableMap<PackageIdentifier, PackageSpecification> singlePackages;
353-
private final ImmutableList<PackageSpecification> negativePackageSpecifications;
354-
private final ImmutableList<PackageSpecification> allSpecifications;
407+
// We separate PackageSpecifications by type.
408+
// - Single positive specs are separate so that we can look them up quickly by package name,
409+
// without requiring a linear search for a satisfying containsPackage().
410+
// - Negative specs need to be separate because their semantics are different (they overrule
411+
// any positive spec).
412+
// We don't bother separating out single negative specs. Negatives are pretty rare anyway.
413+
private final ImmutableMap<PackageIdentifier, PackageSpecification> singlePositives;
414+
private final ImmutableList<PackageSpecification> otherPositives;
415+
private final ImmutableList<PackageSpecification> negatives;
355416

356417
private PackageGroupContents(
357-
ImmutableMap<PackageIdentifier, PackageSpecification> singlePackages,
358-
ImmutableList<PackageSpecification> negativePackageSpecifications,
359-
ImmutableList<PackageSpecification> allSpecifications) {
360-
361-
this.singlePackages = singlePackages;
362-
this.negativePackageSpecifications = negativePackageSpecifications;
363-
this.allSpecifications = allSpecifications;
418+
ImmutableMap<PackageIdentifier, PackageSpecification> singlePositives,
419+
ImmutableList<PackageSpecification> otherPositives,
420+
ImmutableList<PackageSpecification> negatives) {
421+
this.singlePositives = singlePositives;
422+
this.otherPositives = otherPositives;
423+
this.negatives = negatives;
364424
}
365425

366426
/**
@@ -369,84 +429,77 @@ private PackageGroupContents(
369429
*/
370430
public static PackageGroupContents create(
371431
ImmutableList<PackageSpecification> packageSpecifications) {
372-
LinkedHashMap<PackageIdentifier, PackageSpecification> singlePackageBuilder =
373-
new LinkedHashMap<>();
374-
ImmutableList.Builder<PackageSpecification> negativePackageSpecificationsBuilder =
375-
ImmutableList.builder();
376-
ImmutableList.Builder<PackageSpecification> allSpecificationsBuilder =
377-
ImmutableList.builder();
378-
379-
for (PackageSpecification packageSpecification : packageSpecifications) {
380-
if (packageSpecification instanceof SinglePackage) {
381-
singlePackageBuilder.put(
382-
((SinglePackage) packageSpecification).singlePackageName, packageSpecification);
383-
} else if (packageSpecification instanceof NegativePackageSpecification) {
384-
negativePackageSpecificationsBuilder.add(packageSpecification);
432+
ImmutableMap.Builder<PackageIdentifier, PackageSpecification> singlePositives =
433+
ImmutableMap.builder();
434+
ImmutableList.Builder<PackageSpecification> otherPositives = ImmutableList.builder();
435+
ImmutableList.Builder<PackageSpecification> negatives = ImmutableList.builder();
436+
437+
for (PackageSpecification spec : packageSpecifications) {
438+
if (spec instanceof SinglePackage) {
439+
singlePositives.put(((SinglePackage) spec).singlePackageName, spec);
440+
} else if (spec instanceof AllPackages || spec instanceof AllPackagesBeneath) {
441+
otherPositives.add(spec);
442+
} else if (spec instanceof NegativePackageSpecification) {
443+
negatives.add(spec);
385444
} else {
386-
allSpecificationsBuilder.add(packageSpecification);
387-
if (!(packageSpecification instanceof AllPackages)
388-
&& !(packageSpecification instanceof AllPackagesBeneath)) {
389-
throw new IllegalStateException(
390-
"Instance of unhandled class " + packageSpecification.getClass());
391-
}
445+
throw new IllegalStateException(
446+
"Unhandled PackageSpecification subclass " + spec.getClass());
392447
}
393448
}
394449
return new PackageGroupContents(
395-
ImmutableMap.copyOf(singlePackageBuilder),
396-
negativePackageSpecificationsBuilder.build(),
397-
allSpecificationsBuilder.build());
450+
singlePositives.buildKeepingLast(), otherPositives.build(), negatives.build());
398451
}
399452

400453
/**
401-
* Returns {@code true} if the package specifications include the provided {@code packageName}.
402-
* That is, at least one positive package specification matches, and no negative package
403-
* specifications match.
454+
* Returns true if the given package matches at least one of this {@code PackageGroupContents}'
455+
* positive specifications and none of its negative specifications.
404456
*/
405457
public boolean containsPackage(PackageIdentifier packageIdentifier) {
406458
// DO NOT use streams or iterators here as they create excessive garbage.
407459

408-
// if some negative matches, returns false immediately.
409-
for (int i = 0; i < negativePackageSpecifications.size(); i++) {
410-
if (negativePackageSpecifications.get(i).containsPackage(packageIdentifier)) {
460+
// Check negatives first. If there's a match we get to bail out early. If not, we'd still have
461+
// to check all the negatives anyway.
462+
for (int i = 0; i < negatives.size(); i++) {
463+
if (negatives.get(i).containsPackage(packageIdentifier)) {
411464
return false;
412465
}
413466
}
414-
415-
if (singlePackages.containsKey(packageIdentifier)) {
467+
// Check the map in hopes of passing without having to do a linear scan over all other
468+
// positive specs.
469+
if (singlePositives.containsKey(packageIdentifier)) {
416470
return true;
417471
}
418-
419-
for (int i = 0; i < allSpecifications.size(); i++) {
420-
if (allSpecifications.get(i).containsPackage(packageIdentifier)) {
472+
// Oh well.
473+
for (int i = 0; i < otherPositives.size(); i++) {
474+
if (otherPositives.get(i).containsPackage(packageIdentifier)) {
421475
return true;
422476
}
423477
}
424478
return false;
425479
}
426480

427481
/**
428-
* Returns {@link String} representations of the component {@link PackageSpecification}s of the
429-
* same format accepted by {@link #fromString}.
482+
* Maps {@link PackageSpecification#asString} to the component package specs.
483+
*
484+
* <p>Note that strings for specs that cross repositories can't be reparsed using {@link
485+
* PackageSpecification#fromString}.
430486
*/
431-
public Stream<String> containedPackages() {
432-
return getStream().map(PackageSpecification::toString);
487+
public Stream<String> streamPackageStrings(boolean includeDoubleSlash) {
488+
return streamSpecs().map(spec -> spec.asString(includeDoubleSlash));
433489
}
434490

435491
/**
436-
* Returns {@link String} representations of the component {@link PackageSpecification}s of the
437-
* same format accepted by {@link #fromString}.
492+
* Maps {@link PackageSpecification#asStringWithoutRepository} to the component package specs.
438493
*
439-
* <p>The returned {@link String}s are insensitive to the {@link RepositoryName} associated with
440-
* the {@link PackageSpecification}.
494+
* <p>Note that this is ambiguous w.r.t. specs that reference other repositories.
441495
*/
442-
public Stream<String> containedPackagesWithoutRepository() {
443-
return getStream().map(PackageSpecification::toStringWithoutRepository);
496+
public Stream<String> streamPackageStringsWithoutRepository() {
497+
return streamSpecs().map(PackageSpecification::asStringWithoutRepository);
444498
}
445499

446-
private Stream<PackageSpecification> getStream() {
447-
return Stream.concat(
448-
Stream.concat(allSpecifications.stream(), negativePackageSpecifications.stream()),
449-
singlePackages.values().stream());
500+
private Stream<PackageSpecification> streamSpecs() {
501+
return Streams.concat(
502+
otherPositives.stream(), negatives.stream(), singlePositives.values().stream());
450503
}
451504
}
452505
}

src/main/java/com/google/devtools/build/lib/query2/query/output/ProtoOutputFormatter.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,9 @@ public Build.Target toTargetProtoBuffer(Target target, Object extraDataForAttrHa
344344
PackageGroup packageGroup = (PackageGroup) target;
345345
Build.PackageGroup.Builder packageGroupPb =
346346
Build.PackageGroup.newBuilder().setName(packageGroup.getLabel().toString());
347-
for (String containedPackage : packageGroup.getContainedPackages()) {
347+
// TODO(b/77598306): Migrate to format with leading double slash
348+
for (String containedPackage :
349+
packageGroup.getContainedPackages(/*includeDoubleSlash=*/ false)) {
348350
packageGroupPb.addContainedPackage(containedPackage);
349351
}
350352
for (Label include : packageGroup.getIncludes()) {

src/main/java/com/google/devtools/build/lib/query2/query/output/XmlOutputFormatter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,11 @@ private Element createTargetElement(Document doc, Target target)
205205
includes.setAttribute("name", "includes");
206206
elem.appendChild(includes);
207207
Element packages =
208-
createValueElement(doc, Type.STRING_LIST, packageGroup.getContainedPackages());
208+
createValueElement(
209+
doc,
210+
Type.STRING_LIST,
211+
// TODO(b/77598306): Migrate to format with leading double slash
212+
packageGroup.getContainedPackages(/*includeDoubleSlash=*/ false));
209213
packages.setAttribute("name", "packages");
210214
elem.appendChild(packages);
211215
} else if (target instanceof OutputFile) {

0 commit comments

Comments
 (0)