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.render; |
16 | |
17 | import com.google.caja.SomethingWidgyHappenedError; |
18 | import com.google.caja.lexer.FilePosition; |
19 | import com.google.caja.lexer.JsLexer; |
20 | import com.google.caja.lexer.JsTokenQueue; |
21 | import com.google.caja.lexer.JsTokenType; |
22 | import com.google.caja.lexer.Keyword; |
23 | import com.google.caja.lexer.ParseException; |
24 | import com.google.caja.lexer.Token; |
25 | import com.google.caja.lexer.escaping.Escaping; |
26 | import com.google.caja.parser.ParseTreeNode; |
27 | import com.google.caja.parser.js.IntegerLiteral; |
28 | import com.google.caja.parser.js.Operation; |
29 | import com.google.caja.parser.js.Operator; |
30 | import com.google.caja.parser.js.Parser; |
31 | import com.google.caja.parser.js.StringLiteral; |
32 | import com.google.caja.reporting.RenderContext; |
33 | import com.google.caja.util.CajaTestCase; |
34 | import com.google.caja.util.MoreAsserts; |
35 | |
36 | import java.util.ArrayList; |
37 | import java.util.List; |
38 | import java.util.Random; |
39 | |
40 | public class JsMinimalPrinterTest extends CajaTestCase { |
41 | public final void testEmptyBlock() throws Exception { |
42 | assertRendered("{{}}", "{}"); |
43 | } |
44 | |
45 | public final void testAdjacentBlocks() throws Exception { |
46 | assertRendered("{{}{}}", "{}{}"); |
47 | } |
48 | |
49 | public final void testSimpleStatement() throws Exception { |
50 | assertRendered("{foo()}", "foo();"); |
51 | } |
52 | |
53 | public final void testSemisInsideParents() throws Exception { |
54 | assertRendered( |
55 | "{for(var i=0,n=a.length;i<n;++i){" |
56 | + "bar(a[i])}}", |
57 | "for (var i = 0, n = a.length; i < n; ++i) {" |
58 | + " bar(a[ i ]);" |
59 | + "}"); |
60 | } |
61 | |
62 | public final void testObjectConstructor() throws Exception { |
63 | assertRendered( |
64 | "{foo({'x':1,'y':bar({'w':4}),'z':3})}", |
65 | "foo({ x: 1, y: bar({ w: 4 }), z: 3 });"); |
66 | } |
67 | |
68 | public final void testMultipleStatements() throws Exception { |
69 | assertRendered( |
70 | "{(function(a,b,c){foo(a);bar(b);return c})(1,2,3)}", |
71 | "(function (a, b, c) { foo(a); bar(b); return (c); })(1, 2, 3);"); |
72 | } |
73 | |
74 | public final void testMarkupEndStructures() throws Exception { |
75 | // Make sure -->, </script, and ]]> don't show up in rendered output. |
76 | // Preventing these in strings is handled separately. |
77 | assertRendered( |
78 | "{(i--)>j,k< /script>/,[[0]] >0/ / / *x}", |
79 | "i-->j, k</script>/, [[0]]>0 / / / * x;"); |
80 | } |
81 | |
82 | public final void testMarkupStartStructure() throws Exception { |
83 | // Make sure <!-- and <![CDATA[ don't show up in rendered output. |
84 | // Preventing these in strings is handled separately. |
85 | assertRendered( |
86 | "{1< !--b&&c< ![CDATA[0]]}", |
87 | "1<!--b && c<![CDATA[0]]"); |
88 | } |
89 | |
90 | public final void testJSON() throws Exception { |
91 | assertRendered( |
92 | "{({'a':[1,2,3],'b':{'c':[{}],'d':[{'e':null,'f':'foo'},null]}})}", |
93 | "({ a: [1,2,3], b: { c: [{}], d: [{ e: null, f: 'foo' }, null] } });"); |
94 | } |
95 | |
96 | public final void testConditional() throws Exception { |
97 | assertRendered( |
98 | "{if(c1){foo()}else if(c2)bar();else baz()}", |
99 | "if (c1) { foo(); } else if (c2) bar(); else baz();"); |
100 | } |
101 | |
102 | public final void testNumberPropertyAccess() throws Exception { |
103 | assertRendered("{(3).toString()}", "(3).toString();"); |
104 | } |
105 | |
106 | public final void testComments() throws Exception { |
107 | assertLexed( |
108 | "var x=foo;function Bar(){}var baz;a+b", |
109 | |
110 | "" |
111 | + "var x = foo; /* end of line */\n" |
112 | + "/** Own line */\n" |
113 | + "function Bar() {}\n" |
114 | + "/* Beginning */ var baz;\n" |
115 | + "a + // Line comment\n" |
116 | + "b;"); |
117 | } |
118 | |
119 | public final void testDivisionByRegex() throws Exception { |
120 | assertLexed("3/ /foo/", "3 / /foo/;"); |
121 | } |
122 | |
123 | public final void testPunctuationRun() throws Exception { |
124 | assertLexed("!=|| =", "!= || ="); |
125 | } |
126 | |
127 | public final void testNegatedNegativeNumericConstants() { |
128 | assertRendered( |
129 | "-(-3)", // not --3 |
130 | Operation.create( |
131 | FilePosition.UNKNOWN, Operator.NEGATION, |
132 | new IntegerLiteral(FilePosition.UNKNOWN,-3))); |
133 | } |
134 | |
135 | public final void testRetokenization() throws Exception { |
136 | long seed = Long.parseLong( |
137 | System.getProperty("junit.seed", "" + System.currentTimeMillis())); |
138 | Random rnd = new Random(seed); |
139 | boolean pass = false; |
140 | try { |
141 | for (int i = 1000; --i >= 0;) { |
142 | List<String> randomTokens = generateRandomTokens(rnd); |
143 | StringBuilder sb = new StringBuilder(); |
144 | JsMinimalPrinter pp = new JsMinimalPrinter(new Concatenator(sb)); |
145 | for (String token : randomTokens) { |
146 | pp.consume(token); |
147 | } |
148 | pp.noMoreTokens(); |
149 | |
150 | List<String> actualTokens = new ArrayList<String>(); |
151 | try { |
152 | JsLexer lex = new JsLexer(fromString(sb.toString())); |
153 | while (lex.hasNext()) { |
154 | actualTokens.add(lex.next().text); |
155 | } |
156 | } catch (ParseException ex) { |
157 | for (String tok : randomTokens) { |
158 | System.err.println(StringLiteral.toQuotedValue(tok)); |
159 | } |
160 | System.err.println("<<<" + sb + ">>>"); |
161 | throw ex; |
162 | } |
163 | |
164 | MoreAsserts.assertListsEqual(randomTokens, actualTokens); |
165 | } |
166 | pass = true; |
167 | } finally { |
168 | if (!pass) { System.err.println("Using seed " + seed); } |
169 | } |
170 | } |
171 | |
172 | public final void testSpacingAroundBrackets1() { |
173 | assertTokens("longObjectInstance.reallyLongMethodName(a,b,c,d)", |
174 | "longObjectInstance", ".", "reallyLongMethodName", "(", |
175 | "a", ",", "b", ",", "c", ",", "d", ")", ";"); |
176 | } |
177 | |
178 | public final void testSpacingAroundBrackets2() { |
179 | assertTokens("longObjectInstance.reallyLongMethodName(a,b,c,d)", |
180 | "longObjectInstance", ".", "reallyLongMethodName", "(", |
181 | "a", ",", "b", ",", "c", ",", "\n", "d", ")", ";"); |
182 | } |
183 | |
184 | public final void testSpacingAroundBrackets3() { |
185 | assertTokens("longObjectInstance.reallyLongMethodName(a,b,c,d)", |
186 | "longObjectInstance", ".", "reallyLongMethodName", "(", |
187 | "\n", "a", ",", "b", ",", "c", ",", "d", ")", ";"); |
188 | } |
189 | |
190 | public final void testSpacingAroundBrackets4() { |
191 | assertTokens("var x=({'fooBar':[0,1,2,]})", |
192 | "var", "x", "=", "(", "{", "'fooBar'", ":", "[", |
193 | "\n", "0", ",", "1", ",", "2", ",", "]", "}", ")", ";"); |
194 | } |
195 | |
196 | public final void testConfusedTokenSequences() { |
197 | assertTokens("< ! =", "<", "!", "="); |
198 | assertTokens("< !=", "<", "!="); |
199 | } |
200 | |
201 | public final void testNumbersAndDots() { |
202 | assertTokens("2 .toString()", "2", ".", "toString", "(", ")"); |
203 | assertTokens("2..toString()", "2.", ".", "toString", "(", ")"); |
204 | assertTokens("2. .5", "2.", ".5"); |
205 | } |
206 | |
207 | public final void testRestrictedSemicolonInsertion() throws Exception { |
208 | ParseTreeNode node = js(fromString( |
209 | "" |
210 | // 0123456789 |
211 | + "var x=abcd+\n" |
212 | + "+ef;return 1-\n" |
213 | + "-c;if(b)throw new\n" |
214 | + "Error();break label;do\n" |
215 | + "nothing;while(0);continue top;a-\n" |
216 | + "-b;number=counter++" |
217 | + ";number=counter--" |
218 | + ";number=n-++" |
219 | + "counter" |
220 | )); |
221 | StringBuilder out = new StringBuilder(); |
222 | JsMinimalPrinter pp = new JsMinimalPrinter(new Concatenator(out)); |
223 | pp.setLineLengthLimit(10); |
224 | node.render(new RenderContext(pp)); |
225 | pp.noMoreTokens(); |
226 | assertEquals( |
227 | "{var x=abcd+" |
228 | + "\n+ef;return 1-" |
229 | + "\n-c;if(b)throw new" |
230 | + "\nError();break label;do" |
231 | + "\nnothing;while(0);continue top;a-" |
232 | + "\n-b;number=counter++;number=counter--;number=n-++counter}", |
233 | out.toString()); |
234 | } |
235 | |
236 | public final void testNoops() throws ParseException { |
237 | ParseTreeNode b = js(fromString( |
238 | "for (;;) {", |
239 | " if (foo)", |
240 | " bar();", |
241 | " else", |
242 | " ;", |
243 | "}")); |
244 | StringBuilder out = new StringBuilder(); |
245 | JsMinimalPrinter pp = new JsMinimalPrinter(new Concatenator(out)); |
246 | b.render(new RenderContext(pp)); |
247 | pp.noMoreTokens(); |
248 | assertEquals( |
249 | "{for(;;){if(foo)bar();else;}}", |
250 | out.toString()); |
251 | } |
252 | |
253 | private static final JsTokenType[] TYPES = JsTokenType.values(); |
254 | private static final String[] PUNCTUATORS; |
255 | static { |
256 | List<String> puncStrs = new ArrayList<String>(); |
257 | JsLexer.getPunctuationTrie().toStringList(puncStrs); |
258 | PUNCTUATORS = puncStrs.toArray(new String[0]); |
259 | } |
260 | private static final Keyword[] KEYWORDS = Keyword.values(); |
261 | |
262 | private List<String> generateRandomTokens(Random rnd) { |
263 | List<String> tokens = new ArrayList<String>(); |
264 | String last = null; |
265 | for (int i = 10; --i >= 0;) { |
266 | String tok; |
267 | switch (TYPES[rnd.nextInt(TYPES.length)]) { |
268 | case COMMENT: |
269 | continue; |
270 | case STRING: |
271 | tok = StringLiteral.toQuotedValue(randomString(rnd)); |
272 | break; |
273 | case REGEXP: |
274 | // Since regexps are context sensitive, make sure we're in the right |
275 | // context. |
276 | tokens.add("="); |
277 | StringBuilder out = new StringBuilder(); |
278 | out.append('/'); |
279 | Escaping.normalizeRegex(randomString(rnd), false, false, out); |
280 | out.append('/'); |
281 | if (rnd.nextBoolean()) { out.append('g'); } |
282 | if (rnd.nextBoolean()) { out.append('m'); } |
283 | if (rnd.nextBoolean()) { out.append('i'); } |
284 | tok = out.toString(); |
285 | break; |
286 | case PUNCTUATION: |
287 | tok = PUNCTUATORS[rnd.nextInt(PUNCTUATORS.length)]; |
288 | if (tok.startsWith("/")) { |
289 | // Make sure / operators follow numbers so they're not interpreted |
290 | // as regular expressions. |
291 | tokens.add("3"); |
292 | } |
293 | if ("}".equals(tok) && ";".equals(last)) { |
294 | tok = "{"; |
295 | } |
296 | break; |
297 | case WORD: |
298 | tok = randomWord(rnd); |
299 | break; |
300 | case KEYWORD: |
301 | tok = KEYWORDS[rnd.nextInt(KEYWORDS.length)].toString(); |
302 | break; |
303 | case INTEGER: |
304 | int j = rnd.nextInt(Integer.MAX_VALUE); |
305 | switch (rnd.nextInt(3)) { |
306 | case 0: tok = Integer.toString(j, 10); break; |
307 | case 1: tok = "0" + Integer.toString(Math.abs(j), 8); break; |
308 | default: tok = "0x" + Long.toString(Math.abs((long) j), 16); break; |
309 | } |
310 | break; |
311 | case FLOAT: |
312 | tok = "" + Math.abs(rnd.nextFloat()); |
313 | break; |
314 | case LINE_CONTINUATION: |
315 | continue; |
316 | default: |
317 | throw new SomethingWidgyHappenedError(); |
318 | } |
319 | tokens.add(tok); |
320 | last = tok; |
321 | } |
322 | while (";".equals(last)) { |
323 | tokens.remove(tokens.size() - 1); |
324 | last = tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); |
325 | } |
326 | return tokens; |
327 | } |
328 | |
329 | private static final String WORD_CHARS |
330 | = "_$ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; |
331 | private static String randomWord(Random rnd) { |
332 | int len = rnd.nextInt(100) + 1; |
333 | StringBuilder sb = new StringBuilder(len); |
334 | for (int i = 0; i < len; ++i) { |
335 | sb.append(WORD_CHARS.charAt(rnd.nextInt(WORD_CHARS.length()))); |
336 | } |
337 | if (Character.isDigit(sb.charAt(0))) { |
338 | sb.insert(0, '_'); |
339 | } |
340 | return sb.toString(); |
341 | } |
342 | |
343 | private static String randomString(Random rnd) { |
344 | int minCp = 0, maxCp = 0; |
345 | if (rnd.nextBoolean()) { |
346 | minCp = 0x20; |
347 | maxCp = 0x7f; |
348 | } else { |
349 | minCp = 0x0; |
350 | maxCp = 0xd000; |
351 | } |
352 | int len = rnd.nextInt(100) + 1; |
353 | StringBuilder sb = new StringBuilder(len); |
354 | for (int i = 0; i < len; ++i) { |
355 | sb.appendCodePoint(rnd.nextInt(maxCp - minCp) + minCp); |
356 | } |
357 | return sb.toString(); |
358 | } |
359 | |
360 | private void assertRendered(String golden, String input) throws Exception { |
361 | JsLexer lex = new JsLexer(fromString(input)); |
362 | JsTokenQueue tq = new JsTokenQueue(lex, is); |
363 | ParseTreeNode node = new Parser(tq, mq).parse(); |
364 | tq.expectEmpty(); |
365 | |
366 | assertRendered(golden, node); |
367 | } |
368 | |
369 | private void assertRendered(String golden, ParseTreeNode node) { |
370 | StringBuilder out = new StringBuilder(); |
371 | JsMinimalPrinter pp = new JsMinimalPrinter(new Concatenator(out)); |
372 | node.render(new RenderContext(pp)); |
373 | pp.noMoreTokens(); |
374 | |
375 | assertEquals(golden, out.toString()); |
376 | } |
377 | |
378 | private void assertLexed(String golden, String input) throws ParseException { |
379 | StringBuilder out = new StringBuilder(); |
380 | JsMinimalPrinter pp = new JsMinimalPrinter(new Concatenator(out)); |
381 | |
382 | JsLexer lex = new JsLexer(fromString(input)); |
383 | while (lex.hasNext()) { |
384 | Token<JsTokenType> t = lex.next(); |
385 | pp.mark(t.pos); |
386 | pp.consume(t.text); |
387 | } |
388 | pp.noMoreTokens(); |
389 | |
390 | assertEquals(golden, out.toString()); |
391 | } |
392 | |
393 | private void assertTokens(String golden, String... input) { |
394 | StringBuilder out = new StringBuilder(); |
395 | JsMinimalPrinter pp = new JsMinimalPrinter(new Concatenator(out)); |
396 | |
397 | for (String token : input) { |
398 | pp.consume(token); |
399 | } |
400 | pp.noMoreTokens(); |
401 | assertEquals(golden, out.toString()); |
402 | } |
403 | } |