// Copyright (C) 2005 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.caja.lexer;
import com.google.caja.reporting.MessageContext;
import com.google.caja.reporting.MessagePart;
import java.io.IOException;
import java.io.Serializable;
import javax.annotation.Nullable;
/**
* The range of characters in a source file occupied by a token or a group of
* tokens.
*
* <p>Fields are 1 indexed since that is how most text editors display line and
* character (UTF-16 code-unit) positions. The start numbers are inclusive, and
* the end numbers are exclusive, so the number of characters in a token is
* (startCharInFile - endCharInFile).
*
* <p>Line numbers are 1 greater than the number of line breaks where a
* line-break is defined greedily as one of {{{CR}}}, {{{LF}}}, {{{CRLF}}}.
* Bash, javascript, or python style line-continuations like<pre>
* a, b, c = 0, 1, \
* 2
* </pre>
* are treated as line breaks.
* Line numbers are determined solely based on the characters in the file and do
* <b>not</b> represent a logical unit of a program.
*
* <p>File Positions are independent of the language being parsed.
*
* @author mikesamuel@gmail.com
*/
public final class FilePosition implements MessagePart, Serializable {
private static final long serialVersionUID = 1867023076917057795L;
public static final FilePosition UNKNOWN = FilePosition.instance(
InputSource.UNKNOWN, 1, 0, 0);
private final SourceBreaks breaks;
private final int startCharInFile, length;
FilePosition(SourceBreaks breaks, int startCharInFile, int length) {
assert length >= 0 : "length = " + length + " < 0";
this.breaks = breaks;
breaks.makeImmutable();
this.startCharInFile = startCharInFile;
this.length = length;
}
SourceBreaks getBreaks() { return breaks; }
public InputSource source() { return breaks.source(); }
/**
* 1 greater than the number of newlines between the start of the token and
* the beginning of the file.
*/
public int startLineNo() { return breaks.lineAt(startCharInFile); }
/**
* 1 greater than the number of characters since the beginning of the file.
*/
public int startCharInFile() { return startCharInFile; }
/**
* 1 greater than the number of characters since the last newline character.
*/
public int startCharInLine() { return breaks.charInLineAt(startCharInFile); }
public int endLineNo() { return breaks.lineAt(endCharInFile()); }
public int endCharInFile() { return startCharInFile + length; }
public int endCharInLine() { return breaks.charInLineAt(endCharInFile()); }
public int length() { return length; }
public FilePosition narrowTo(int offset, int length) {
return breaks.toFilePosition(
startCharInFile + offset, startCharInFile + offset + length);
}
public static FilePosition between(FilePosition a, FilePosition b) {
if (a.getBreaks() != b.getBreaks()) { return FilePosition.UNKNOWN; }
int start = a.startCharInFile() + a.length();
return new FilePosition(a.getBreaks(), start, b.startCharInFile() - start);
}
public static FilePosition instance(
InputSource source, int lineNo, int charInFile, int charInLine) {
return instance(source, lineNo, charInFile, charInLine, 0);
}
public static FilePosition instance(
InputSource source, int lineNo,
int charInFile, int charInLine, int length) {
SourceBreaks breaks = new SourceBreaks(source, lineNo - 1);
breaks.lineStartsAt(charInFile - charInLine + 1);
return breaks.toFilePosition(charInFile, charInFile + length);
}
/** Produce a FilePosition divorced from SourceBreaks to ease testing. */
public static FilePosition fromLinePositions(
InputSource source,
int startLineNo, int startCharInLine, int endLineNo, int endCharInLine) {
int lineNo = startLineNo - 1;
SourceBreaks b = new SourceBreaks(source, lineNo);
// Construct source breaks as if we were parsing a file.
int charInFile = 1;
int charInLine = 1;
while (lineNo < startLineNo) {
b.lineStartsAt(++charInFile);
++lineNo;
}
int delta = startCharInLine - charInLine;
charInFile += delta;
charInLine += delta;
int startCharInFile = charInFile;
while (lineNo < endLineNo) {
charInLine = 1;
b.lineStartsAt(++charInFile);
++lineNo;
}
delta = endCharInLine - charInLine;
charInFile += delta;
charInLine += delta;
int endCharInFile = charInFile;
return b.toFilePosition(startCharInFile, endCharInFile);
}
public static FilePosition startOfFile(InputSource source) {
return instance(source, 1, 1, 1);
}
public static FilePosition span(FilePosition start, FilePosition end) {
if (start == end) { return start; }
if (!start.source().equalsAndNotUnknown(end.source())) {
return FilePosition.UNKNOWN;
}
return start.getBreaks().toFilePosition(
start.startCharInFile(), end.endCharInFile());
}
public static FilePosition startOf(FilePosition fp) {
if (fp.length() == 0) { return fp; }
return new FilePosition(fp.getBreaks(), fp.startCharInFile(), 0);
}
public static FilePosition endOf(FilePosition fp) {
if (fp.length() == 0) { return fp; }
return new FilePosition(
fp.getBreaks(), fp.startCharInFile() + fp.length(), 0);
}
public static FilePosition endOfOrNull(@Nullable FilePosition fp) {
return fp != null ? endOf(fp) : null;
}
public final void format(MessageContext context, Appendable out)
throws IOException {
source().format(context, out);
out.append(":")
.append(String.valueOf(this.startLineNo()))
.append("+")
.append(String.valueOf(this.startCharInLine()));
if (this.startCharInFile() != this.endCharInFile()) {
out.append(" - ");
if (this.startLineNo() != this.endLineNo()) {
out.append(String.valueOf(this.endLineNo()))
.append("+");
}
out.append(String.valueOf(this.endCharInLine()));
}
}
public final void formatShort(Appendable out) throws IOException {
MessageContext mc = new MessageContext();
InputSource is = source();
mc.addInputSource(is);
out.append(mc.abbreviate(is))
.append(":")
.append(String.valueOf(this.startLineNo()));
}
@Override
public final String toString() {
String fn = this.source().getUri().toString();
StringBuilder sb = new StringBuilder();
sb.append(fn.substring(fn.lastIndexOf('/') + 1))
.append(':').append(this.startLineNo()).append('+')
.append(this.startCharInLine()).append('@')
.append(this.startCharInFile());
if (this.startCharInFile() != this.endCharInFile()) {
sb.append(" - ");
if (this.endLineNo() != this.startLineNo()) {
sb.append(this.endLineNo()).append('+');
}
sb.append(this.endCharInLine()).append('@')
.append(this.endCharInFile());
}
return sb.toString();
}
@Override
public final boolean equals(@Nullable Object o) {
if (!(o instanceof FilePosition)) { return false; }
FilePosition that = (FilePosition) o;
// equals and hashCode depend only on information available
// without walking the newline array.
return (
this.startCharInFile == that.startCharInFile
&& this.length == that.length
&& this.source().equals(that.source())
);
}
@Override
public final int hashCode() {
return (
this.source().hashCode()
^ this.startCharInFile
^ this.length
);
}
}