1 | // Copyright (C) 2008 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.reporting; |
16 | |
17 | import com.google.caja.SomethingWidgyHappenedError; |
18 | import com.google.caja.lexer.FilePosition; |
19 | import com.google.caja.lexer.InputSource; |
20 | |
21 | import java.io.IOException; |
22 | import java.util.Map; |
23 | |
24 | /** |
25 | * Given original source code, produces snippets for error messages. |
26 | * <p> |
27 | * For a {@link Message} message like {@code file:16+10-13: bar not defined}, |
28 | * the snippet might look like |
29 | * <pre> |
30 | * file:16 var foo = bar() + baz |
31 | * ^^^ |
32 | * </pre> |
33 | * |
34 | * @author mikesamuel@gmail.com |
35 | */ |
36 | public class SnippetProducer { |
37 | private static final int DEFAULT_MAX_WIDTH = 80; |
38 | private static final int DEFAULT_TAB_WIDTH = 8; |
39 | |
40 | private final Map<InputSource, ? extends CharSequence> originalSource; |
41 | protected final MessageContext mc; |
42 | protected final int maxWidth, tabWidth; |
43 | |
44 | public SnippetProducer( |
45 | Map<InputSource, ? extends CharSequence> originalSource, |
46 | MessageContext mc) { |
47 | this(originalSource, mc, DEFAULT_MAX_WIDTH, DEFAULT_TAB_WIDTH); |
48 | } |
49 | |
50 | public SnippetProducer( |
51 | Map<InputSource, ? extends CharSequence> originalSource, |
52 | MessageContext mc, |
53 | int maxWidth) { |
54 | this(originalSource, mc, maxWidth, DEFAULT_TAB_WIDTH); |
55 | } |
56 | |
57 | public SnippetProducer( |
58 | Map<InputSource, ? extends CharSequence> originalSource, |
59 | MessageContext mc, int maxWidth, int tabWidth) { |
60 | this.originalSource = originalSource; |
61 | this.mc = mc; |
62 | this.maxWidth = maxWidth; |
63 | this.tabWidth = tabWidth; |
64 | } |
65 | |
66 | public final String getSnippet(Message msg) { |
67 | StringBuilder snippet = new StringBuilder(); |
68 | for (MessagePart mp : msg.getMessageParts()) { |
69 | if (!(mp instanceof FilePosition)) { continue; } |
70 | FilePosition pos = (FilePosition) mp; |
71 | int len = snippet.length(); |
72 | if (len != 0) { snippet.append('\n'); } |
73 | int snippetStart = snippet.length(); |
74 | try { |
75 | appendSnippet(pos, snippet); |
76 | } catch (IOException ex) { |
77 | throw new SomethingWidgyHappenedError( |
78 | "StringBuilders shouldn't throw IOExceptions", ex); |
79 | } |
80 | // If no content written by appendSnippet, then remove the newline. |
81 | if (snippet.length() == snippetStart) { snippet.setLength(len); } |
82 | } |
83 | return snippet.toString(); |
84 | } |
85 | |
86 | public final void appendSnippet(FilePosition pos, Appendable out) |
87 | throws IOException { |
88 | InputSource src = pos.source(); |
89 | CharSequence sourceCode = originalSource.get(src); |
90 | if (sourceCode == null) { return; } // Can't write. |
91 | |
92 | // Pick a representative line from pos. |
93 | int lineNo = pos.startLineNo(); |
94 | int start = pos.startCharInLine() - 1; |
95 | CharSequence line = fetchLine(sourceCode, lineNo); |
96 | |
97 | if (line != null |
98 | && (line.length() == 0 || isLinebreak(line.charAt(0))) |
99 | && lineNo + 1 <= pos.endLineNo()) { |
100 | // If the start of the pos is a newline, advance to the next. |
101 | ++lineNo; |
102 | start = 0; |
103 | line = fetchLine(sourceCode, lineNo); |
104 | } |
105 | if (line == null) { return; } |
106 | |
107 | // Be paranoid about position since we don't want bad positions or errors |
108 | // in the originalSource map to prevent us from reporting errors at all. |
109 | start = Math.min(line.length(), start); |
110 | int end = Math.max( |
111 | Math.min((pos.endLineNo() == lineNo |
112 | ? pos.endCharInLine() - 1 : Integer.MAX_VALUE), |
113 | line.length()), |
114 | start); |
115 | |
116 | // Reduce line to maxWidth of context. |
117 | if (0 < maxWidth && maxWidth < line.length()) { |
118 | end = Math.min(end, start + maxWidth); |
119 | int left = Math.max(0, end - maxWidth); |
120 | int right = Math.min(line.length(), left + maxWidth); |
121 | line = line.subSequence(left, right); |
122 | start -= left; |
123 | end -= left; |
124 | } |
125 | |
126 | formatSnippet(pos, |
127 | FilePosition.instance(src, lineNo, 1, line.length() + 1), |
128 | line, start, end, out); |
129 | } |
130 | |
131 | /** |
132 | * May be overridden to format a snippet differently, e.g. by HTML escaping |
133 | * line and inserting tags around {@code line[start:end]}. |
134 | * |
135 | * @param errorPosition actual unmodified error fileposition |
136 | * @param snippetPos line granularity error position of the snippet |
137 | * @param end >= start. Implementations should take care to provide some |
138 | * useful information if end == start, since a zero length range might |
139 | * be used to indicate where information is missing, or where inferred |
140 | * content was inserted. |
141 | * @throws IOException only if out raised an IOException. |
142 | */ |
143 | protected void formatSnippet(FilePosition errorPosition, |
144 | FilePosition snippetPos, CharSequence line, int start, int end, |
145 | Appendable out) |
146 | throws IOException { |
147 | // Write out "file:14: <line-of-sourcecode>" |
148 | StringBuilder posBuf = new StringBuilder(); |
149 | formatFilePosition(snippetPos, posBuf); |
150 | posBuf.append(": "); |
151 | int filePosLength = posBuf.length(); |
152 | |
153 | int nSpaces = start + filePosLength; |
154 | int nCarets = end - start; |
155 | |
156 | out.append(posBuf); |
157 | // Expand tabs so that the carets line up with the source. |
158 | int nExtraSpaces = expandTabs(line, 0, start, 0, out); |
159 | int nExtraCarets = expandTabs(line, start, end, nExtraSpaces, out); |
160 | expandTabs(line, end, line.length(), nExtraSpaces + nExtraCarets, out); |
161 | if (line.length() == 0 || !isLinebreak(line.charAt(line.length() - 1))) { |
162 | // If the line is the last in the file, it may not end with a newline. |
163 | out.append("\n"); |
164 | } |
165 | repeat(" ", nSpaces + nExtraSpaces, out); |
166 | repeat("^^^^^^^^^^^^^^^^", Math.max(nCarets + nExtraCarets, 1), out); |
167 | } |
168 | |
169 | /** |
170 | * May be overridden to format a position differently, e.g. by inserting links |
171 | * to source files. |
172 | */ |
173 | protected void formatFilePosition(FilePosition pos, Appendable out) |
174 | throws IOException { |
175 | pos.source().format(mc, out); |
176 | out.append(":"); |
177 | out.append(String.valueOf(pos.startLineNo())); |
178 | } |
179 | |
180 | /** Append count characters from pattern onto out, repeating if necessary. */ |
181 | private static void repeat(String pattern, int count, Appendable out) |
182 | throws IOException { |
183 | while (count >= pattern.length()) { |
184 | out.append(pattern); |
185 | count -= pattern.length(); |
186 | } |
187 | if (count > 0) { out.append(pattern, 0, count); } |
188 | } |
189 | |
190 | |
191 | // The scheme below does not take into account different languages' |
192 | // different definitions of newline, but it does use the same scheme as |
193 | // CharProducer's language agnostic line counting scheme which agrees |
194 | // with source code editors. |
195 | // CharProducer does not bump the lineNo counter on codepoints 0x2028,2029. |
196 | private static CharSequence fetchLine(CharSequence seq, int lineNo) { |
197 | int pos = 0; |
198 | for (int i = lineNo; --i >= 1;) { |
199 | pos = posPastNextLinebreak(seq, pos); |
200 | } |
201 | int start = pos; |
202 | int end = posPastNextLinebreak(seq, pos); |
203 | if (start < end) { |
204 | return seq.subSequence(start, end); |
205 | } |
206 | return null; |
207 | } |
208 | |
209 | private static int indexOf( |
210 | CharSequence seq, char ch, int fromIndex, int toIndex) { |
211 | for (int i = fromIndex; i < toIndex; ++i) { |
212 | if (seq.charAt(i) == ch) { return i; } |
213 | } |
214 | return -1; |
215 | } |
216 | |
217 | private int expandTabs( |
218 | CharSequence seq, int start, int end, int nExpanded, Appendable out) |
219 | throws IOException { |
220 | final String SPACES = " "; |
221 | int tabIdx = indexOf(seq, '\t', start, end); |
222 | if (tabIdx < 0) { |
223 | out.append(seq, start, end); |
224 | return 0; |
225 | } |
226 | int nExtra = 0; |
227 | int done = start; |
228 | do { |
229 | out.append(seq, done, tabIdx); |
230 | int nBefore = nExtra + tabIdx + nExpanded; |
231 | int nSpaces = tabWidth - (nBefore % tabWidth); |
232 | nExtra += nSpaces - 1; |
233 | while (nSpaces >= SPACES.length()) { |
234 | out.append(SPACES); |
235 | nSpaces -= SPACES.length(); |
236 | } |
237 | out.append(SPACES, 0, nSpaces); |
238 | done = tabIdx + 1; |
239 | } while ((tabIdx = indexOf(seq, '\t', done, end)) >= 0); |
240 | out.append(seq, done, end); |
241 | return nExtra; |
242 | } |
243 | |
244 | private static int posPastNextLinebreak(CharSequence seq, int pos) { |
245 | int len = seq.length(); |
246 | for (;pos < len; ++pos) { |
247 | char ch = seq.charAt(pos); |
248 | if (ch == '\n') { return pos + 1; } |
249 | if (ch == '\r') { |
250 | return pos + ((pos + 1 < len && '\n' == seq.charAt(pos + 1)) ? 2 : 1); |
251 | } |
252 | } |
253 | return len; |
254 | } |
255 | |
256 | private static boolean isLinebreak(char ch) { |
257 | return ch == '\r' || ch == '\n'; |
258 | } |
259 | } |