18
18
19
19
import static com .google .common .base .Preconditions .checkArgument ;
20
20
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 ;
24
23
import 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 ;
29
24
import java .util .Set ;
30
- import java .util .regex .Matcher ;
31
- import java .util .regex .Pattern ;
32
25
import javax .annotation .Nullable ;
33
26
34
27
/**
@@ -57,12 +50,10 @@ public boolean isRemoveLine() {
57
50
public static class Applier {
58
51
private final CharSequence source ;
59
52
private final EndPosTable endPositions ;
60
- private final Supplier <ImmutableSortedMap <Integer , Integer >> lineOffsets ;
61
53
62
54
public Applier (CharSequence source , EndPosTable endPositions ) {
63
55
this .source = source ;
64
56
this .endPositions = endPositions ;
65
- this .lineOffsets = Suppliers .memoize (() -> lineOffsets (source .toString ()));
66
57
}
67
58
68
59
/**
@@ -71,86 +62,77 @@ public Applier(CharSequence source, EndPosTable endPositions) {
71
62
*/
72
63
@ Nullable
73
64
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
+ }
79
73
80
- Set <Integer > modifiedLines = new HashSet <>();
74
+ StringBuilder replaced = new StringBuilder ();
75
+ int positionInOriginal = 0 ;
81
76
for (Replacement repl : replacements ) {
82
77
checkArgument (
83
78
repl .endPosition () <= source .length (),
84
79
"End [%s] should not exceed source length [%s]" ,
85
80
repl .endPosition (),
86
81
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
- }
92
82
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 ();
97
89
}
90
+ // Flush out any remaining content after the final change
91
+ replaced .append (source , positionInOriginal , source .length ());
98
92
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 );
125
97
}
126
- return new AppliedFix (snippet , isRemoveLine );
98
+ return new AppliedFix (snippet , /* isRemoveLine= */ false );
127
99
}
128
100
129
101
/** 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 ) {
131
103
Replacements replacements = new Replacements ();
132
104
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 ;
134
132
}
135
133
}
136
134
137
135
public static Applier fromSource (CharSequence source , EndPosTable endPositions ) {
138
136
return new Applier (source , endPositions );
139
137
}
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
- }
156
138
}
0 commit comments