| // Copyright 2014 The Bazel Authors. All rights reserved. | |
| // | |
| // 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.devtools.build.lib.syntax; | |
| import com.google.common.collect.Ordering; | |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; | |
| import com.google.devtools.build.lib.events.Location.LineAndColumn; | |
| import com.google.devtools.build.lib.util.Pair; | |
| import com.google.devtools.build.lib.util.Preconditions; | |
| import com.google.devtools.build.lib.util.StringUtilities; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import java.io.Serializable; | |
| import java.nio.CharBuffer; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.Comparator; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.regex.Matcher; | |
| import java.util.regex.Pattern; | |
| /** | |
| * A table to keep track of line numbers in source files. The client creates a LineNumberTable for | |
| * their buffer using {@link #create}. The client can then ask for the line and column given a | |
| * position using ({@link #getLineAndColumn(int)}). | |
| */ | |
| public abstract class LineNumberTable implements Serializable { | |
| /** | |
| * Returns the (line, column) pair for the specified offset. | |
| */ | |
| abstract LineAndColumn getLineAndColumn(int offset); | |
| /** | |
| * Returns the (start, end) offset pair for a specified line, not including | |
| * newline chars. | |
| */ | |
| abstract Pair<Integer, Integer> getOffsetsForLine(int line); | |
| /** | |
| * Returns the path corresponding to the given offset. | |
| */ | |
| abstract PathFragment getPath(int offset); | |
| static LineNumberTable create(char[] buffer, PathFragment path) { | |
| // If #line appears within a BUILD file, we assume it has been preprocessed | |
| // by gconfig2blaze. We ignore all actual newlines and compute the logical | |
| // LNT based only on the presence of #line markers. | |
| return StringUtilities.containsSubarray(buffer, "\n#line ".toCharArray()) | |
| ? new HashLine(buffer, path) | |
| : new Regular(buffer, path); | |
| } | |
| /** | |
| * Line number table implementation for regular source files. Records | |
| * offsets of newlines. | |
| */ | |
| @Immutable | |
| public static class Regular extends LineNumberTable { | |
| /** | |
| * A mapping from line number (line >= 1) to character offset into the file. | |
| */ | |
| private final int[] linestart; | |
| private final PathFragment path; | |
| private final int bufferLength; | |
| public Regular(char[] buffer, PathFragment path) { | |
| // Compute the size. | |
| int size = 2; | |
| for (int i = 0; i < buffer.length; i++) { | |
| if (buffer[i] == '\n') { | |
| size++; | |
| } | |
| } | |
| linestart = new int[size]; | |
| int index = 0; | |
| linestart[index++] = 0; // The 0th line does not exist - so we fill something in | |
| // to make sure the start pos for the 1st line ends up at | |
| // linestart[1]. Using 0 is useful for tables that are | |
| // completely empty. | |
| linestart[index++] = 0; // The first line ("line 1") starts at offset 0. | |
| // Scan the buffer and record the offset of each line start. Doing this | |
| // once upfront is faster than checking each char as it is pulled from | |
| // the buffer. | |
| for (int i = 0; i < buffer.length; i++) { | |
| if (buffer[i] == '\n') { | |
| linestart[index++] = i + 1; | |
| } | |
| } | |
| this.bufferLength = buffer.length; | |
| this.path = path; | |
| } | |
| private int getLineAt(int offset) { | |
| if (offset < 0) { | |
| throw new IllegalStateException("Illegal position: " + offset); | |
| } | |
| int lowBoundary = 1, highBoundary = linestart.length - 1; | |
| while (true) { | |
| if ((highBoundary - lowBoundary) <= 1) { | |
| if (linestart[highBoundary] > offset) { | |
| return lowBoundary; | |
| } else { | |
| return highBoundary; | |
| } | |
| } | |
| int medium = lowBoundary + ((highBoundary - lowBoundary) >> 1); | |
| if (linestart[medium] > offset) { | |
| highBoundary = medium; | |
| } else { | |
| lowBoundary = medium; | |
| } | |
| } | |
| } | |
| @Override | |
| LineAndColumn getLineAndColumn(int offset) { | |
| int line = getLineAt(offset); | |
| int column = offset - linestart[line] + 1; | |
| return new LineAndColumn(line, column); | |
| } | |
| @Override | |
| PathFragment getPath(int offset) { | |
| return path; | |
| } | |
| @Override | |
| Pair<Integer, Integer> getOffsetsForLine(int line) { | |
| if (line <= 0 || line >= linestart.length) { | |
| throw new IllegalArgumentException("Illegal line: " + line); | |
| } | |
| return Pair.of(linestart[line], line < linestart.length - 1 | |
| ? linestart[line + 1] | |
| : bufferLength); | |
| } | |
| @Override | |
| public int hashCode() { | |
| return Objects.hash(Arrays.hashCode(linestart), path, bufferLength); | |
| } | |
| @Override | |
| public boolean equals(Object other) { | |
| if (other == null || !other.getClass().equals(getClass())) { | |
| return false; | |
| } | |
| Regular that = (Regular) other; | |
| return this.bufferLength == that.bufferLength | |
| && Arrays.equals(this.linestart, that.linestart) | |
| && Objects.equals(this.path, that.path); | |
| } | |
| } | |
| /** | |
| * Line number table implementation for source files that have been | |
| * preprocessed. Ignores newlines and uses only #line directives. | |
| */ | |
| @Immutable | |
| public static class HashLine extends LineNumberTable { | |
| /** | |
| * Represents a "#line" directive | |
| */ | |
| private static class SingleHashLine implements Serializable { | |
| private final int offset; | |
| private final int line; | |
| private final PathFragment path; | |
| SingleHashLine(int offset, int line, PathFragment path) { | |
| this.offset = offset; | |
| this.line = line; | |
| this.path = path; | |
| } | |
| } | |
| private static Ordering<SingleHashLine> hashOrdering = Ordering.from( | |
| new Comparator<SingleHashLine>() { | |
| @Override | |
| public int compare(SingleHashLine o1, SingleHashLine o2) { | |
| return Integer.compare(o1.offset, o2.offset); | |
| } | |
| }); | |
| private static final Pattern pattern = Pattern.compile("\n#line ([0-9]+) \"([^\"\\n]+)\""); | |
| private final List<SingleHashLine> table; | |
| private final PathFragment defaultPath; | |
| private final int bufferLength; | |
| public HashLine(char[] buffer, PathFragment defaultPath) { | |
| CharSequence bufString = CharBuffer.wrap(buffer); | |
| Matcher m = pattern.matcher(bufString); | |
| List<SingleHashLine> unorderedTable = new ArrayList<>(); | |
| Map<String, PathFragment> pathCache = new HashMap<>(); | |
| while (m.find()) { | |
| String pathString = m.group(2); | |
| PathFragment pathFragment = pathCache.get(pathString); | |
| if (pathFragment == null) { | |
| pathFragment = defaultPath.getRelative(pathString); | |
| pathCache.put(pathString, pathFragment); | |
| } | |
| unorderedTable.add(new SingleHashLine( | |
| m.start(0) + 1, //offset (+1 to skip \n in pattern) | |
| Integer.parseInt(m.group(1)), // line number | |
| pathFragment)); // filename is an absolute path | |
| } | |
| this.table = hashOrdering.immutableSortedCopy(unorderedTable); | |
| this.bufferLength = buffer.length; | |
| this.defaultPath = Preconditions.checkNotNull(defaultPath); | |
| } | |
| private SingleHashLine getHashLine(int offset) { | |
| if (offset < 0) { | |
| throw new IllegalStateException("Illegal position: " + offset); | |
| } | |
| int binarySearchIndex = | |
| Collections.binarySearch(table, new SingleHashLine(offset, -1, null), hashOrdering); | |
| if (binarySearchIndex >= 0) { | |
| // An exact match in the binary search. Return it. | |
| return table.get(binarySearchIndex); | |
| } else if (binarySearchIndex < -1) { | |
| // See docs at Collections#binarySearch. Our index is -(insertionPoint + 1). To get to the | |
| // nearest existing value in the original list, we must subtract 2. | |
| return table.get(-binarySearchIndex - 2); | |
| } else { | |
| return null; | |
| } | |
| } | |
| @Override | |
| LineAndColumn getLineAndColumn(int offset) { | |
| SingleHashLine hashLine = getHashLine(offset); | |
| return hashLine == null ? new LineAndColumn(-1, 1) : new LineAndColumn(hashLine.line, 1); | |
| } | |
| @Override | |
| PathFragment getPath(int offset) { | |
| SingleHashLine hashLine = getHashLine(offset); | |
| return hashLine == null ? defaultPath : hashLine.path; | |
| } | |
| /** | |
| * Returns 0, 0 for an unknown line | |
| */ | |
| @Override | |
| Pair<Integer, Integer> getOffsetsForLine(int line) { | |
| for (int ii = 0, len = table.size(); ii < len; ii++) { | |
| if (table.get(ii).line == line) { | |
| return Pair.of(table.get(ii).offset, ii < len - 1 | |
| ? table.get(ii + 1).offset | |
| : bufferLength); | |
| } | |
| } | |
| return Pair.of(0, 0); | |
| } | |
| @Override | |
| public int hashCode() { | |
| return Objects.hash(table, defaultPath, bufferLength); | |
| } | |
| @Override | |
| public boolean equals(Object other) { | |
| if (other == null || !other.getClass().equals(getClass())) { | |
| return false; | |
| } | |
| HashLine that = (HashLine) other; | |
| return this.bufferLength == that.bufferLength | |
| && this.defaultPath.equals(that.defaultPath) | |
| && this.table.equals(that.table); | |
| } | |
| } | |
| } |