/* * Copyright 2008 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.common.css; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.primitives.Ints; import javax.annotation.Nullable; /** * A location in source code. A location is a sequence of adjacent characters * that usually represent a token or a larger language construct. * * <p>Error messages represent the most common use of this class, as an error * message usually relates to a source code location. The location related to * some messages might not be known; this class has a special value that * represents an "unknown" location. * * <p>Character sequences this class points to can have 0 length. If that is the * case, the actual location is that "point" between two source code characters. * This usually means that something happens from that point onwards, or that an * error has been detected at that point but there is no information regarding * the actual token that caused the error. * * <p>Instances of this class are immutable. * */ public class SourceCodeLocation implements Comparable<SourceCodeLocation> { /** * This describes a point in a string. A point is the location between two * characters and is indicated by the character index of the immediately * following character. For example, in the string "abc", point 0 refers to * the location immediately before the 'a' and point 2 to the location before * the 'c'. * * <p>For convenience we also store the line number of the point (the line * that contains the character following the point) and the index in that * line. Both of these indices start at 1. The first line of a file is line 1 * and the point before its first character has index 1 on line 1. * * <p>The exact definition of lines depends on the language conventions and is * best left for the parser to handle. If you want to display the text at that * location, either use the character index or use the line number and the * index in the line together with lines as they were split by the parser. * * <p>It might happen that the source code point for something is not known. * This is modeled by a point with the special value -1 for the character * index. The line and the index on the line must be 0 in this case. * * <p>Instances of this class are immutable. */ @VisibleForTesting public static class SourceCodePoint implements Comparable<SourceCodePoint> { /** * The index of the character immediately after the source code point. * Indices start at 0; -1 means the location is not known. */ private final int characterIndex; /** * The number of the line that contains the character at characterIndex. * Numbers start at 1; 0 means the location is not known. */ private final int lineNumber; /** * The index in the line identified by lineNumber of the character at * characterIndex. Numbers start at 1; 0 means the location is not known. */ private final int indexInLine; SourceCodePoint(int characterIndex, int lineNumber, int indexInLine) { this.lineNumber = lineNumber; this.indexInLine = indexInLine; this.characterIndex = characterIndex; if (!hasValidKnownCoordinates() && !hasValidUnknownCoordinates()) { throw new IllegalArgumentException( String.format( "The location passed " + "(lineNumber %d, indexInLine %d, characterIndex %d) " + "is not valid.", lineNumber, indexInLine, characterIndex)); } if (!hasPlausibleCoordinates()) { throw new IllegalArgumentException( String.format( "The location passed " + "(lineNumber %d, indexInLine %d, characterIndex %d) " + "is not plausible.", lineNumber, indexInLine, characterIndex)); } } SourceCodePoint(SourceCodePoint that) { this(that.characterIndex, that.lineNumber, that.indexInLine); } boolean hasValidKnownCoordinates() { return lineNumber >= 1 && indexInLine >= 1 && characterIndex >= 0; } boolean hasValidUnknownCoordinates() { return characterIndex == -1 && lineNumber == 0 && indexInLine == 0; } boolean hasPlausibleCoordinates() { return characterIndex >= lineNumber - 1 + indexInLine - 1; } int getCharacterIndex() { return characterIndex; } int getLineNumber() { return lineNumber; } int getIndexInLine() { return indexInLine; } boolean isUnknown() { return characterIndex == -1; } @Override public boolean equals(@Nullable Object o) { if (o == null) { return false; } if (!(o instanceof SourceCodePoint)) { return false; } SourceCodePoint other = (SourceCodePoint) o; boolean areEqual = this.characterIndex == other.characterIndex; if (areEqual) { Preconditions.checkState((this.lineNumber == other.lineNumber) && (this.indexInLine == other.indexInLine), "Character indexes are equal but line numbers and indexes within " + "the line do not match."); } else { Preconditions.checkState((this.lineNumber != other.lineNumber) || (this.indexInLine != other.indexInLine), "Line numbers and indexes within the line match but character " + "indexes are not equal"); } return areEqual; } @Override public int hashCode() { return characterIndex; } @Override public int compareTo(SourceCodePoint o) { Preconditions.checkNotNull(o); return Ints.compare(this.characterIndex, o.characterIndex); } } private static final SourceCode UNKNOWN_SOURCE_CODE = new SourceCode("unknown", ""); /** * Returns an unknown location. */ public static SourceCodeLocation getUnknownLocation() { SourceCodeLocation result = new SourceCodeLocation( UNKNOWN_SOURCE_CODE, -1 /* beginCharacterIndex */, 0 /* beginLineNumber */, 0 /* beginIndexInLine */, -1 /* endCharacterindex */, 0 /* endLineNumber */, 0 /* endIndexInLine */); Preconditions.checkState(result.isUnknown()); Preconditions.checkState(result.begin.hasValidUnknownCoordinates()); Preconditions.checkState(result.end.hasValidUnknownCoordinates()); return result; } private final SourceCode sourceCode; /** * The sequence starts at the character immediately following the begin point. */ private final SourceCodePoint begin; /** * The sequence ends at the character immediately before the end point. The * character immediately after the end point (the one indicated by the end's * {@link SourceCodePoint#characterIndex}) is not part of the sequence. The * empty sequence's begin point and end point are the same: * {@code begin.equals(end)}. */ private final SourceCodePoint end; @VisibleForTesting public SourceCodeLocation(SourceCode sourceCode, int beginCharacterIndex, int beginLineNumber, int beginIndexInLine, int endCharacterIndex, int endLineNumber, int endIndexInLine) { Preconditions.checkNotNull(sourceCode); this.sourceCode = sourceCode; this.begin = new SourceCodePoint(beginCharacterIndex, beginLineNumber, beginIndexInLine); this.end = new SourceCodePoint(endCharacterIndex, endLineNumber, endIndexInLine); Preconditions.checkArgument(begin.compareTo(end) <= 0, "Beginning location must come before the end location."); } public SourceCode getSourceCode() { return sourceCode; } public boolean isUnknown() { Preconditions.checkState(begin.isUnknown() == end.isUnknown()); return begin.isUnknown(); } public int getBeginCharacterIndex() { return begin.getCharacterIndex(); } public int getBeginLineNumber() { return begin.getLineNumber(); } public int getBeginIndexInLine() { return begin.getIndexInLine(); } public int getEndCharacterIndex() { return end.getCharacterIndex(); } public int getEndLineNumber() { return end.getLineNumber(); } public int getEndIndexInLine() { return end.getIndexInLine(); } public int getCharacterIndex() { return getBeginCharacterIndex(); } public int getLineNumber() { return getBeginLineNumber(); } public int getIndexInLine() { return getBeginIndexInLine(); } public SourceCodePoint getBegin() { return begin; } public SourceCodePoint getEnd() { return end; } @Override public boolean equals(@Nullable Object o) { if (o == null) { return false; } if (!(o instanceof SourceCodeLocation)) { return false; } SourceCodeLocation other = (SourceCodeLocation) o; return sourceCode == other.sourceCode && begin.equals(other.begin) && end.equals(other.end); } @Override public int hashCode() { return sourceCode.hashCode() ^ begin.hashCode() ^ (end.hashCode() << 16); } /** * Comparison and ordering of locations for source code in different * input files is supported because we don't always preserve * locations during AST mutations and yet we still want to be able * to sort error reports, doing the best job we can for the errors * that have known locations. For the semantics of this method, see * {@link Comparable#compareTo(Object)}. */ @Override public int compareTo(SourceCodeLocation o) { Preconditions.checkNotNull(o); if (sourceCode != o.sourceCode) { if (sourceCode == null) { return -1; } else if (o.sourceCode == null) { return 1; } else { return sourceCode.hashCode() - o.sourceCode.hashCode(); } } int startPointsComparison = begin.compareTo(o.begin); if (startPointsComparison != 0) { return startPointsComparison; } return end.compareTo(o.end); } }