blob: 5d9d2c2b4e7a2d9fd44dc095cd4b3beb38e24b90 [file] [log] [blame]
// Copyright (C) 2019 The Android Open Source Project
//
// 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.gerrit.server.fixes;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.FixReplacement;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.jgit.diff.ReplaceEdit;
import com.google.gerrit.server.patch.Text;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.eclipse.jgit.diff.Edit;
/**
* Produces final version of an input content with all fixes applied together with list of edits.
*/
public class FixCalculator {
private static final Comparator<FixReplacement> ASC_RANGE_FIX_REPLACEMENT_COMPARATOR =
Comparator.comparing(fixReplacement -> fixReplacement.range);
private FixCalculator() {}
/**
* Returns a result of applying fixes to an original content.
*
* @param originalContent is a text to which fixes must be applied
* @param fixReplacements is a list of fixes to be applied
* @throws ResourceConflictException if the fixReplacements contains invalid data (for example, if
* an item points to an invalid range or if some ranges are intersected).
*/
public static String getNewFileContent(
String originalContent, List<FixReplacement> fixReplacements)
throws ResourceConflictException {
FixResult fixResult = calculateFix(new Text(originalContent.getBytes(UTF_8)), fixReplacements);
return fixResult.text.getString(0, fixResult.text.size(), false);
}
/**
* Returns a result of applying fixes to an original content and list of applied edits.
*
* @param originalText is a text to which fixes must be applied
* @param fixReplacements is a list of fixes to be applied
* @return {@link FixResult}
* @throws ResourceConflictException if the fixReplacements contains invalid data (for example, if
* an item points to an invalid range or if some ranges are intersected).
*/
public static FixResult calculateFix(Text originalText, List<FixReplacement> fixReplacements)
throws ResourceConflictException {
List<FixReplacement> sortedReplacements = new ArrayList<>(fixReplacements);
sortedReplacements.sort(ASC_RANGE_FIX_REPLACEMENT_COMPARATOR);
if (!sortedReplacements.isEmpty() && sortedReplacements.get(0).range.startLine <= 0) {
throw new ResourceConflictException(
String.format(
"Cannot calculate fix replacement for range %s",
toString(sortedReplacements.get(0).range)));
}
ContentBuilder builder = new ContentBuilder(originalText);
for (FixReplacement fixReplacement : sortedReplacements) {
try {
builder.addReplacement(fixReplacement);
} catch (IndexOutOfBoundsException e) {
throw new ResourceConflictException(
String.format(
"Cannot calculate fix replacement for range %s", toString(fixReplacement.range)),
e);
}
}
return builder.build();
}
private static String toString(Comment.Range range) {
return String.format(
"(%s:%s - %s:%s)", range.startLine, range.startChar, range.endLine, range.endChar);
}
private static class ContentBuilder {
private static class FixRegion {
int startSrcLine;
int startDstLine;
int startSrcPos;
int startDstPos;
List<Edit> internalEdits;
FixRegion() {
this.internalEdits = new ArrayList<>();
}
}
private final ContentProcessor contentProcessor;
final ImmutableList.Builder<Edit> edits;
FixRegion currentRegion;
ContentBuilder(Text src) {
this.contentProcessor = new ContentProcessor(src);
this.edits = new ImmutableList.Builder<>();
}
void addReplacement(FixReplacement replacement) {
if (shouldStartNewEdit(replacement)) {
finishExistingEdit();
}
// processSrcContent expects that line number is 0-based,
// but replacement.range.startLine is 1-based, so subtract 1
processSrcContent(replacement.range.startLine - 1, replacement.range.startChar, true);
processReplacement(replacement);
}
Text getNewText() {
return new Text(contentProcessor.sb.toString().getBytes(UTF_8));
}
void finish() {
finishExistingEdit();
if (contentProcessor.hasMoreLines()) {
contentProcessor.appendLinesToEndOfContent();
}
}
public FixResult build() {
finish();
return new FixResult(edits.build(), this.getNewText());
}
private void finishExistingEdit() {
if (contentProcessor.srcPosition.column > 0 || contentProcessor.dstPosition.column > 0) {
contentProcessor.processToEndOfLine(true);
}
if (currentRegion != null) {
int endSrc = contentProcessor.srcPosition.line;
if (contentProcessor.srcPosition.column > 0) {
endSrc++;
}
int endDst = contentProcessor.dstPosition.line;
if (contentProcessor.dstPosition.column > 0) {
endDst++;
}
ReplaceEdit edit =
new ReplaceEdit(
currentRegion.startSrcLine,
endSrc,
currentRegion.startDstLine,
endDst,
currentRegion.internalEdits);
currentRegion = null;
edits.add(edit);
}
}
private boolean shouldStartNewEdit(FixReplacement replacement) {
if (currentRegion == null) {
return true;
}
// New edit must be started if there is at least one unchanged line after the last edit
// Subtract 1 from replacement.range.startLine because it is a 1-based line number,
// and contentProcessor.srcPosition.line is a 0-based line number
return replacement.range.startLine - 1 > contentProcessor.srcPosition.line + 1;
}
private void processSrcContent(int toLine, int toColumn, boolean append)
throws IndexOutOfBoundsException {
// toLine >= currentSrcLineIndex
if (toLine == contentProcessor.srcPosition.line) {
contentProcessor.processLineToColumn(toColumn, append);
} else {
contentProcessor.processToEndOfLine(append);
contentProcessor.processMultiline(toLine, append);
contentProcessor.processLineToColumn(toColumn, append);
}
}
private void processReplacement(FixReplacement fix) {
if (currentRegion == null) {
currentRegion = new FixRegion();
currentRegion.startSrcLine = contentProcessor.srcPosition.line;
currentRegion.startSrcPos = contentProcessor.srcPosition.getLineStartPos();
currentRegion.startDstLine = contentProcessor.dstPosition.line;
currentRegion.startDstPos = contentProcessor.dstPosition.getLineStartPos();
}
int srcStartPos = contentProcessor.srcPosition.textPos;
int dstStartPos = contentProcessor.dstPosition.textPos;
contentProcessor.appendReplacement(fix.replacement);
processSrcContent(fix.range.endLine - 1, fix.range.endChar, false);
currentRegion.internalEdits.add(
new Edit(
srcStartPos - currentRegion.startSrcPos,
contentProcessor.srcPosition.textPos - currentRegion.startSrcPos,
dstStartPos - currentRegion.startDstPos,
contentProcessor.dstPosition.textPos - currentRegion.startDstPos));
}
}
private static class ContentProcessor {
static class ContentPosition {
int line;
int column;
int textPos;
void appendMultilineContent(int lineCount, int charCount) {
line += lineCount;
column = 0;
textPos += charCount;
}
void appendLineEndedWithEOLMark(int charCount) {
textPos += charCount;
line++;
column = 0;
}
void appendStringWithoutEOLMark(int charCount) {
textPos += charCount;
column += charCount;
}
int getLineStartPos() {
return textPos - column;
}
}
private final StringBuilder sb;
final ContentPosition srcPosition;
final ContentPosition dstPosition;
String currentSrcLine;
Text src;
boolean endOfSource;
ContentProcessor(Text src) {
this.src = src;
sb = new StringBuilder(src.size());
srcPosition = new ContentPosition();
dstPosition = new ContentPosition();
endOfSource = src.size() == 0;
}
void processMultiline(int toLine, boolean append) {
if (endOfSource || toLine <= srcPosition.line) {
return;
}
int fromLine = srcPosition.line;
String lines = src.getString(fromLine, toLine, false);
int lineCount = toLine - fromLine;
int charCount = lines.length();
srcPosition.appendMultilineContent(lineCount, charCount);
if (append) {
sb.append(lines);
dstPosition.appendMultilineContent(lineCount, charCount);
}
currentSrcLine = null;
endOfSource = srcPosition.line >= src.size();
}
void processToEndOfLine(boolean append) {
if (endOfSource) {
return;
}
String srcLine = getCurrentSrcLine();
int from = srcPosition.column;
int charCount = srcLine.length() - from;
boolean lastLineNoEOLMark = srcPosition.line >= src.size() - 1 && src.isMissingNewlineAtEnd();
if (!lastLineNoEOLMark) {
srcPosition.appendLineEndedWithEOLMark(charCount);
endOfSource = srcPosition.line >= src.size();
} else {
srcPosition.appendStringWithoutEOLMark(charCount);
endOfSource = true;
}
if (append) {
sb.append(srcLine, from, srcLine.length());
if (!lastLineNoEOLMark) {
dstPosition.appendLineEndedWithEOLMark(charCount);
} else {
dstPosition.appendStringWithoutEOLMark(charCount);
}
}
currentSrcLine = null;
}
void processLineToColumn(int to, boolean append) throws IndexOutOfBoundsException {
if (to == 0) {
return;
}
String srcLine = getCurrentSrcLine();
if (to > srcLine.length()) {
throw new IndexOutOfBoundsException("Parameter to is out of string");
} else if (to == srcLine.length()) {
if (srcPosition.line < src.size() - 1 || !src.isMissingNewlineAtEnd()) {
throw new IndexOutOfBoundsException("The processLineToColumn shouldn't add end of line");
}
}
int from = srcPosition.column;
int charCount = to - from;
srcPosition.appendStringWithoutEOLMark(charCount);
if (append) {
sb.append(srcLine, from, to);
dstPosition.appendStringWithoutEOLMark(charCount);
}
}
void appendLinesToEndOfContent() {
processMultiline(src.size(), true);
}
void appendReplacement(String replacement) {
if (replacement.length() == 0) {
return;
}
sb.append(replacement);
int lastNewLinePos = -1;
int newLineMarkCount = 0;
while (true) {
int index = replacement.indexOf('\n', lastNewLinePos + 1);
if (index < 0) {
break;
}
lastNewLinePos = index;
newLineMarkCount++;
}
if (newLineMarkCount > 0) {
dstPosition.appendMultilineContent(newLineMarkCount, lastNewLinePos + 1);
}
dstPosition.appendStringWithoutEOLMark(replacement.length() - lastNewLinePos - 1);
}
boolean hasMoreLines() {
return !endOfSource;
}
private String getCurrentSrcLine() {
if (currentSrcLine == null) {
currentSrcLine = src.getString(srcPosition.line, srcPosition.line + 1, false);
}
return currentSrcLine;
}
}
/** The result of applying fix to a file content */
public static class FixResult {
/** List of edits to transform an original text to a final text (with all fixes applied) */
public final ImmutableList<Edit> edits;
/** Final text with all applied fixes */
public final Text text;
FixResult(ImmutableList<Edit> edits, Text text) {
this.edits = edits;
this.text = text;
}
}
}