// Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). // Licensed under the Apache License, Version 2.0 (see LICENSE). package com.twitter.intellij.pants.psi.reference; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiReference; import com.intellij.util.PathUtil; import com.jetbrains.python.psi.PyStringLiteralExpression; import com.twitter.intellij.pants.util.PantsUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.ObjectUtils; /** * Converts a python string literal in a build file into PantsPsiReferences */ public class PantsTargetReferenceSet { @NotNull private final PyStringLiteralExpression myStringLiteralExpression; private List<PsiReference> myReferences; public PantsTargetReferenceSet(@NotNull PyStringLiteralExpression element) { myStringLiteralExpression = element; } protected List<PsiReference> getReferences() { if (myReferences == null) { myReferences = getFileReferences(myStringLiteralExpression); } return myReferences; } @NotNull private List<PsiReference> getFileReferences(@NotNull PyStringLiteralExpression expression) { final Optional<VirtualFile> buildRoot = PantsUtil.findBuildRoot(expression.getContainingFile()); if (!buildRoot.isPresent()) { return Collections.emptyList(); } final PartialTargetAddress address = PartialTargetAddress.parse(expression.getStringValue()); final List<PsiReference> result = new ArrayList<PsiReference>(); result.addAll(createPathSegments(expression, address.normalizedPath)); if (address.explicitTarget != null) { result.add(createTargetSegmentReference(expression, address)); } return result; } @NotNull private List<PsiReference> createPathSegments(@NotNull PyStringLiteralExpression expression, @NotNull String path) { final List<PsiReference> result = new ArrayList<PsiReference>(); int prevIndex = 0; for (int i = 0; i < path.length(); ++i) { if (path.charAt(i) != '/') continue; result.add( createPathSegmentReference(expression, path, prevIndex, i) ); prevIndex = i + 1; } return result; } @NotNull private PantsVirtualFileReference createPathSegmentReference( @NotNull PyStringLiteralExpression expression, @NotNull String path, int prevIndex, int endIndex ) { final TextRange range = TextRange.create( expression.valueOffsetToTextOffset(prevIndex), expression.valueOffsetToTextOffset(endIndex) ); return new PantsVirtualFileReference( myStringLiteralExpression, range, path.substring(prevIndex, endIndex), path.substring(0, endIndex) ); } @NotNull private PantsTargetReference createTargetSegmentReference( @NotNull PyStringLiteralExpression expression, @NotNull PartialTargetAddress address ) { final TextRange range = TextRange.create( expression.valueOffsetToTextOffset(address.startOfExplicitTarget()), expression.valueOffsetToTextOffset(address.valueLength) ); return new PantsTargetReference( myStringLiteralExpression, range, address.explicitTarget, address.normalizedPath ); } public static class PartialTargetAddress { @Nullable private final String explicitTarget; @NotNull private final String normalizedPath; private final int valueLength; private final int colonIndex; @Nullable public String getExplicitTarget() { return explicitTarget; } @NotNull public String getNormalizedPath() { return normalizedPath; } public PartialTargetAddress(@Nullable String explicitTarget, @NotNull String normalizedPath, int valueLength, int colonIndex) { this.explicitTarget = explicitTarget; this.normalizedPath = normalizedPath; this.valueLength = valueLength; this.colonIndex = colonIndex; } @NotNull public static PartialTargetAddress parse(String value) { final int colonIndex = value.indexOf(':'); final int valueLength = value.length(); //substringAfter may return empty string if colon is the last character, so null is need in this case final String explicitTarget = StringUtil.nullize(StringUtil.substringAfter(value, ":")); //substringBefore may return null if colon does not exist, so rawPath is value in this case final String rawPath = ObjectUtils.notNull(StringUtil.substringBefore(value, ":"), value); String normalizedPath; if (rawPath.isEmpty()) { normalizedPath = rawPath; } else { final String normalized = PathUtil.toSystemIndependentName(rawPath); normalizedPath = normalized.charAt(normalized.length() - 1) == '/' ? normalized : normalized + "/"; } return new PartialTargetAddress(explicitTarget, normalizedPath, valueLength, colonIndex); } int startOfExplicitTarget() { return colonIndex + 1; } } }