1818
1919import static com .google .common .base .Preconditions .checkArgument ;
2020
21- import com .google .common .base .Supplier ;
22- import com .google .common .base .Suppliers ;
23- import com .google .common .collect .ImmutableSortedMap ;
21+ import com .google .common .collect .ImmutableSet ;
22+ import com .google .common .collect .Iterables ;
2423import com .sun .tools .javac .tree .EndPosTable ;
25- import java .io .IOException ;
26- import java .io .LineNumberReader ;
27- import java .io .StringReader ;
28- import java .util .HashSet ;
2924import java .util .Set ;
30- import java .util .regex .Matcher ;
31- import java .util .regex .Pattern ;
3225import javax .annotation .Nullable ;
3326
3427/**
@@ -57,12 +50,10 @@ public boolean isRemoveLine() {
5750 public static class Applier {
5851 private final CharSequence source ;
5952 private final EndPosTable endPositions ;
60- private final Supplier <ImmutableSortedMap <Integer , Integer >> lineOffsets ;
6153
6254 public Applier (CharSequence source , EndPosTable endPositions ) {
6355 this .source = source ;
6456 this .endPositions = endPositions ;
65- this .lineOffsets = Suppliers .memoize (() -> lineOffsets (source .toString ()));
6657 }
6758
6859 /**
@@ -71,86 +62,77 @@ public Applier(CharSequence source, EndPosTable endPositions) {
7162 */
7263 @ Nullable
7364 public AppliedFix apply (Fix suggestedFix ) {
74- StringBuilder replaced = new StringBuilder (source );
75-
76- // We have to apply the replacements in descending order, since otherwise the positions in
77- // subsequent replacements are invalidated by earlier replacements.
78- Set <Replacement > replacements = descending (suggestedFix .getReplacements (endPositions ));
65+ // We apply the replacements in ascending order here. Descending is simpler, since applying a
66+ // replacement can't change the index for future replacements, but it leads to quadratic
67+ // copying behavior as we constantly shift the tail of the file around in our StringBuilder.
68+ ImmutableSet <Replacement > replacements =
69+ ascending (suggestedFix .getReplacements (endPositions ));
70+ if (replacements .isEmpty ()) {
71+ return null ;
72+ }
7973
80- Set <Integer > modifiedLines = new HashSet <>();
74+ StringBuilder replaced = new StringBuilder ();
75+ int positionInOriginal = 0 ;
8176 for (Replacement repl : replacements ) {
8277 checkArgument (
8378 repl .endPosition () <= source .length (),
8479 "End [%s] should not exceed source length [%s]" ,
8580 repl .endPosition (),
8681 source .length ());
87- replaced .replace (repl .startPosition (), repl .endPosition (), repl .replaceWith ());
88-
89- // Find the line number(s) being modified
90- modifiedLines .add (lineOffsets .get ().floorEntry (repl .startPosition ()).getValue ());
91- }
9282
93- // Not sure this is really the right behavior, but otherwise we can end up with an infinite
94- // loop below.
95- if (modifiedLines .isEmpty ()) {
96- return null ;
83+ // Write the unmodified content leading up to this change
84+ replaced .append (source , positionInOriginal , repl .startPosition ());
85+ // And the modified content for this change
86+ replaced .append (repl .replaceWith ());
87+ // Then skip everything from source between start and end
88+ positionInOriginal = repl .endPosition ();
9789 }
90+ // Flush out any remaining content after the final change
91+ replaced .append (source , positionInOriginal , source .length ());
9892
99- LineNumberReader lineNumberReader =
100- new LineNumberReader (new StringReader (replaced .toString ()));
101- String snippet = null ;
102- boolean isRemoveLine = false ;
103- try {
104- while (!modifiedLines .contains (lineNumberReader .getLineNumber ())) {
105- lineNumberReader .readLine ();
106- }
107- // TODO: this is over-simplified; need a failing test case
108- snippet = lineNumberReader .readLine ();
109- if (snippet == null ) {
110- // The file's last line was removed.
111- snippet = "" ;
112- } else {
113- snippet = snippet .trim ();
114- // snip comment from line
115- if (snippet .contains ("//" )) {
116- snippet = snippet .substring (0 , snippet .indexOf ("//" )).trim ();
117- }
118- }
119- if (snippet .isEmpty ()) {
120- isRemoveLine = true ;
121- snippet = "to remove this line" ;
122- }
123- } catch (IOException e ) {
124- // impossible since source is in-memory
93+ // Find the changed line containing the first edit
94+ String snippet = firstEditedLine (replaced , Iterables .get (replacements , 0 ));
95+ if (snippet .isEmpty ()) {
96+ return new AppliedFix ("to remove this line" , /* isRemoveLine= */ true );
12597 }
126- return new AppliedFix (snippet , isRemoveLine );
98+ return new AppliedFix (snippet , /* isRemoveLine= */ false );
12799 }
128100
129101 /** Get the replacements in an appropriate order to apply correctly. */
130- private static Set <Replacement > descending (Set <Replacement > set ) {
102+ private static ImmutableSet <Replacement > ascending (Set <Replacement > set ) {
131103 Replacements replacements = new Replacements ();
132104 set .forEach (replacements ::add );
133- return replacements .descending ();
105+ return replacements .ascending ();
106+ }
107+
108+ /**
109+ * Finds the full text of the first line that's changed. In this case "line" means "bracketed by
110+ * \n characters". We don't handle \r\n specially, because the strings that javac provides to
111+ * Error Prone have already been transformed from platform line endings to newlines (and even if
112+ * it didn't, the dangling \r characters would be handled by a trim() call).
113+ */
114+ private static String firstEditedLine (StringBuilder content , Replacement firstEdit ) {
115+ // We subtract 1 here because we want to find the first newline *before* the edit, not one
116+ // at its beginning.
117+ int startOfFirstEditedLine = content .lastIndexOf ("\n " , firstEdit .startPosition () - 1 );
118+ int endOfFirstEditedLine = content .indexOf ("\n " , firstEdit .startPosition ());
119+ if (startOfFirstEditedLine == -1 ) {
120+ startOfFirstEditedLine = 0 ; // Change to start of file with no preceding newline
121+ }
122+ if (endOfFirstEditedLine == -1 ) {
123+ // Change to last line of file
124+ endOfFirstEditedLine = content .length ();
125+ }
126+ String snippet = content .substring (startOfFirstEditedLine , endOfFirstEditedLine );
127+ snippet = snippet .trim ();
128+ if (snippet .contains ("//" )) {
129+ snippet = snippet .substring (0 , snippet .indexOf ("//" )).trim ();
130+ }
131+ return snippet ;
134132 }
135133 }
136134
137135 public static Applier fromSource (CharSequence source , EndPosTable endPositions ) {
138136 return new Applier (source , endPositions );
139137 }
140-
141- private static final Pattern NEWLINE = Pattern .compile ("\\ R" );
142-
143- /** Returns the start offsets of the lines in the input. */
144- private static ImmutableSortedMap <Integer , Integer > lineOffsets (String input ) {
145- ImmutableSortedMap .Builder <Integer , Integer > lines = ImmutableSortedMap .naturalOrder ();
146- int line = 0 ;
147- int idx = 0 ;
148- lines .put (idx , line ++);
149- Matcher matcher = NEWLINE .matcher (input );
150- while (matcher .find (idx )) {
151- idx = matcher .end ();
152- lines .put (idx , line ++);
153- }
154- return lines .buildOrThrow ();
155- }
156138}
0 commit comments