/* * Copyright 2010 Google Inc. * * 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.template.soy.base; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.auto.value.AutoValue; import com.google.common.base.CharMatcher; import com.google.common.collect.ComparisonChain; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; /** * Describes a source location in a Soy input file. * * @author brndn@google.com (Brendan Linn) */ @ParametersAreNonnullByDefault public final class SourceLocation implements Comparable<SourceLocation> { /** A file path or URI useful for error messages. */ @Nonnull private final String filePath; private final String fileName; private final Point begin; private final Point end; /** * A nullish source location. * * @deprecated There is no reason to use this other than laziness. Soy has complete source * location information. */ @Deprecated public static final SourceLocation UNKNOWN = new SourceLocation("unknown"); /** * @param filePath A file path or URI useful for error messages. * @param beginLine The line number in the source file where this location begins (1-based), or -1 * if associated with the entire file instead of a line. * @param beginColumn The column number in the source file where this location begins (1-based), * or -1 if associated with the entire file instead of a line. * @param endLine The line number in the source file where this location ends (1-based), or -1 if * associated with the entire file instead of a line. * @param endColumn The column number in the source file where this location ends (1-based), or -1 * if associated with the entire file instead of a line. */ public SourceLocation( String filePath, int beginLine, int beginColumn, int endLine, int endColumn) { this(filePath, Point.create(beginLine, beginColumn), Point.create(endLine, endColumn)); } public SourceLocation(String filePath) { this(filePath, -1, -1, -1, -1); } public SourceLocation(String filePath, Point begin, Point end) { int lastBangIndex = filePath.lastIndexOf('!'); if (lastBangIndex != -1) { // This is a resource in a JAR file. Only keep everything after the bang. filePath = filePath.substring(lastBangIndex + 1); } this.fileName = fileNameFromPath(filePath); this.filePath = filePath; this.begin = checkNotNull(begin); this.end = checkNotNull(end); } /** Extracts the file name from a path. */ public static String fileNameFromPath(String filePath) { // TODO(lukes): consider using Java 7 File APIs here. int lastSlashIndex = CharMatcher.anyOf("/\\").lastIndexIn(filePath); if (lastSlashIndex != -1 && lastSlashIndex != filePath.length() - 1) { return filePath.substring(lastSlashIndex + 1); } return filePath; } /** * Returns a file path or URI useful for error messages. This should not be used to fetch content * from the file system. */ @Nonnull public String getFilePath() { return filePath; } @Nullable public String getFileName() { if (UNKNOWN.equals(this)) { // This is to replicate old behavior where SoyFileNode#getFileName returns null when an // invalid SoyFileNode is created. return null; } return fileName; } /** Returns the line number in the source file where this location begins (1-based). */ public int getBeginLine() { return begin.line(); } /** Returns the column number in the source file where this location begins (1-based). */ public int getBeginColumn() { return begin.column(); } /** Returns the line number in the source file where this location ends (1-based). */ public int getEndLine() { return end.line(); } /** Returns the column number in the source file where this location ends (1-based). */ public int getEndColumn() { return end.column(); } /** * True iff this location is known, i.e. not the special value {@link #UNKNOWN}. * * @deprecated For the same reason that {@link #UNKNOWN} is. */ @Deprecated public boolean isKnown() { return !this.equals(UNKNOWN); } @Override public int compareTo(SourceLocation o) { // TODO(user): use Comparator.comparing(...) return ComparisonChain.start() .compare(this.filePath, o.filePath) .compare(this.begin, o.begin) // These last two are unlikely to make a difference, but if they do it means we sort smaller // source locations first. .compare(this.end, o.end) .result(); } @Override public boolean equals(@Nullable Object o) { if (!(o instanceof SourceLocation)) { return false; } SourceLocation that = (SourceLocation) o; return this.filePath.equals(that.filePath) && this.begin.equals(that.begin) && this.end.equals(that.end); } @Override public int hashCode() { return filePath.hashCode() + 31 * begin.hashCode() + 31 * 31 * end.hashCode(); } @Override public String toString() { return begin.line() != -1 ? (filePath + ":" + begin.line() + ":" + begin.column()) : filePath; } public SourceLocation offsetStartCol(int offset) { return new SourceLocation(filePath, begin.offset(0, offset), end); } public SourceLocation offsetEndCol(int offset) { return new SourceLocation(filePath, begin, end.offset(0, offset)); } /** * Returns a new SourceLocation that starts where this SourceLocation starts and ends where {@code * other} ends. */ public SourceLocation extend(SourceLocation other) { checkState( filePath.equals(other.filePath), "Mismatched files paths: %s and %s", filePath, other.filePath); return new SourceLocation(filePath, begin, other.end); } /** * Returns a new SourceLocation that starts where this SourceLocation starts and ends {@code * lines} and {@code cols} further than where it ends. */ public SourceLocation extend(int lines, int cols) { return new SourceLocation(filePath, begin, end.offset(lines, cols)); } /** Returns a new location that points to the first character of this location. */ public SourceLocation getBeginLocation() { return new SourceLocation(filePath, begin, begin); } public SourceLocation.Point getBeginPoint() { return begin; } /** Returns a new location that points to the last character of this location. */ public SourceLocation getEndLocation() { return new SourceLocation(filePath, end, end); } public SourceLocation.Point getEndPoint() { return end; } /** A Point in a source file. */ @AutoValue public abstract static class Point implements Comparable<Point> { public static final Point UNKNOWN_POINT = new AutoValue_SourceLocation_Point(-1, -1); public static Point create(int line, int column) { if (line == -1 && column == -1) { return UNKNOWN_POINT; } checkArgument(line > 0); checkArgument(column > 0); return new AutoValue_SourceLocation_Point(line, column); } public abstract int line(); public abstract int column(); public Point offset(int byLines, int byColumns) { if (line() == -1) { return this; } return Point.create(line() + byLines, column() + byColumns); } public SourceLocation asLocation(String filePath) { return new SourceLocation(filePath, this, this); } @Override public int compareTo(Point o) { return ComparisonChain.start() .compare(line(), o.line()) .compare(column(), o.column()) .result(); } } }