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.ancillary.linter; |
16 | |
17 | import com.google.caja.lexer.InputSource; |
18 | import com.google.caja.parser.js.Block; |
19 | import com.google.caja.reporting.Message; |
20 | import com.google.caja.reporting.MessageQueue; |
21 | import com.google.caja.reporting.SimpleMessageQueue; |
22 | import com.google.caja.util.CajaTestCase; |
23 | import com.google.caja.util.Lists; |
24 | import com.google.caja.util.MoreAsserts; |
25 | import com.google.caja.util.Sets; |
26 | |
27 | import java.net.URI; |
28 | import java.util.Arrays; |
29 | import java.util.Collections; |
30 | import java.util.List; |
31 | import java.util.Set; |
32 | |
33 | public class LinterTest extends CajaTestCase { |
34 | |
35 | public final void testProvides() throws Exception { |
36 | runLinterTest( |
37 | jobs(new LintJobMaker(js(fromString("var x;"))).make()), |
38 | "LINT: testProvides:1+1 - 6: Undocumented global x"); |
39 | runLinterTest( |
40 | jobs(new LintJobMaker(js(fromString("var x = 1;"))) |
41 | .withProvides("x") |
42 | .make()) |
43 | ); |
44 | runLinterTest( |
45 | jobs(new LintJobMaker(js(fromString("var x = 1;"))) |
46 | .withProvides("x", "y") |
47 | .make()), |
48 | "ERROR: testProvides: @provides y not provided" |
49 | ); |
50 | runLinterTest( |
51 | jobs(new LintJobMaker(js(fromString( |
52 | "" |
53 | + "var x = 1;\n" |
54 | + "var y;\n" |
55 | + "if (Math.random()) { y = 1; }" // Not always assigned. |
56 | ))) |
57 | .withProvides("x", "y") |
58 | .make()), |
59 | "ERROR: testProvides: @provides y not provided" |
60 | ); |
61 | runLinterTest( |
62 | jobs(new LintJobMaker(js(fromString("for (var i;;) {}"))).make()), |
63 | "LINT: testProvides:1+6 - 11: Undocumented global i"); |
64 | } |
65 | |
66 | public final void testMultiplyProvidedSymbols() throws Exception { |
67 | runLinterTest( |
68 | jobs(new LintJobMaker(js(fromString( |
69 | "var foo = 0, bar = 1;", new InputSource(URI.create("test:///f1")) |
70 | ))) |
71 | .withProvides("foo", "bar") |
72 | .make(), |
73 | new LintJobMaker(js(fromString( |
74 | "var bar = 2, baz = 4;", new InputSource(URI.create("test:///f2")) |
75 | ))) |
76 | .withProvides("bar", "baz") |
77 | .make()), |
78 | "ERROR: f2: Another input, f1, already @provides bar"); |
79 | } |
80 | |
81 | public final void testRequires() throws Exception { |
82 | runLinterTest( |
83 | jobs(new LintJobMaker(js(fromString("x();"))).make()), |
84 | "ERROR: testRequires:1+1 - 2: Symbol x has not been defined"); |
85 | runLinterTest( |
86 | jobs(new LintJobMaker(js(fromString("x();"))) |
87 | .withRequires("x") |
88 | .make()) |
89 | ); |
90 | runLinterTest( |
91 | jobs(new LintJobMaker(js(fromString("x();"))) |
92 | .withRequires("x", "y") |
93 | .make()), |
94 | "ERROR: testRequires: @requires y not used" |
95 | ); |
96 | } |
97 | |
98 | public final void testOverrides() throws Exception { |
99 | runLinterTest( |
100 | jobs(new LintJobMaker(js(fromString( |
101 | "Object = Array;\n"))) |
102 | .make()), |
103 | "ERROR: testOverrides:1+1 - 7: Invalid assignment to Object"); |
104 | runLinterTest( |
105 | jobs(new LintJobMaker(js(fromString( |
106 | "Date.now = function () { return (new Date).getTime(); };\n"))) |
107 | .make()), |
108 | "ERROR: testOverrides:1+1 - 5: Invalid assignment to Date.now"); |
109 | runLinterTest( |
110 | jobs(new LintJobMaker(js(fromString( |
111 | "Date.now = function () { return (new Date).getTime(); };\n"))) |
112 | .withOverrides("Date") |
113 | .make())); |
114 | runLinterTest( |
115 | jobs(new LintJobMaker(js(fromString( |
116 | "var Date;\n"))) |
117 | .withOverrides("Date") |
118 | .make())); |
119 | // No override needed when used as a RHS |
120 | runLinterTest( |
121 | jobs(new LintJobMaker(js(fromString( |
122 | "var data = {}; data[Date] = new Date();\n"))) |
123 | .withProvides("data") |
124 | .make())); |
125 | } |
126 | |
127 | public final void testFunctionScopes() throws Exception { |
128 | runLinterTest( |
129 | jobs(new LintJobMaker(js(fromString( |
130 | "" |
131 | + "function p(b, c) {\n" |
132 | + " var d;\n" |
133 | + " d = r * a - b + c * r;\n" |
134 | + " return d;" |
135 | + "}"))) |
136 | .withProvides("p") |
137 | .withRequires("r") |
138 | .make()), |
139 | "ERROR: testFunctionScopes:3+11 - 12: Symbol a has not been defined"); |
140 | } |
141 | |
142 | public final void testRedefinition() throws Exception { |
143 | runLinterTest( |
144 | jobs(new LintJobMaker(js(fromString( |
145 | "(function () { var a = 1; var a = 2; })();"))).make()), |
146 | ("ERROR: testRedefinition:1+27 - 36: " |
147 | + "a originally defined at testRedefinition:1+16 - 25")); |
148 | } |
149 | |
150 | public final void testCatchBlocks() throws Exception { |
151 | runLinterTest( |
152 | jobs(new LintJobMaker(js(fromString( |
153 | "" |
154 | + "try {\n" |
155 | + " throw caution();\n" |
156 | + "} catch (e) {\n" |
157 | + " if (e.to === THE_WIND) {\n" |
158 | + " panic(e);\n" |
159 | + " }\n" |
160 | + "}"))) |
161 | .withRequires("caution", "THE_WIND") |
162 | .make()), |
163 | "ERROR: testCatchBlocks:5+5 - 10: Symbol panic has not been defined"); |
164 | runLinterTest( |
165 | jobs(new LintJobMaker(js(fromString( |
166 | "" |
167 | + "try {\n" |
168 | + " throw new Error();\n" |
169 | + "} catch (e) {\n" |
170 | + " // swallow exception\n" |
171 | + "}"))) |
172 | .withProvides("e") |
173 | .make()), |
174 | "ERROR: testCatchBlocks: @provides e not provided"); |
175 | runLinterTest( |
176 | jobs(new LintJobMaker(js(fromString( |
177 | "" |
178 | + "var e;\n" |
179 | + "try {\n" |
180 | + " throw new Error();\n" |
181 | + "} catch (e) {\n" |
182 | + " // swallow exception\n" |
183 | + "}"))) |
184 | .make()), |
185 | "LINT: testCatchBlocks:1+1 - 6: Undocumented global e", |
186 | ("WARNING: testCatchBlocks:4+10 - 11: Declaration of e masks" |
187 | + " declaration at testCatchBlocks:1+1 - 6")); |
188 | runLinterTest( |
189 | jobs(new LintJobMaker(js(fromString( |
190 | "" |
191 | + "try {\n" |
192 | + " throw new Error();\n" |
193 | + "} catch (e) {\n" |
194 | + " var f = e.stack;\n" |
195 | + " print(f);\n" |
196 | + "}"))) |
197 | .withRequires("print") |
198 | .make()), |
199 | "LINT: testCatchBlocks:4+3 - 18: Undocumented global f"); |
200 | runLinterTest( |
201 | jobs(new LintJobMaker(js(fromString( |
202 | "" |
203 | + "function g(e, f) {\n" |
204 | + " try {\n" |
205 | + " f();\n" |
206 | + " } catch (e) {\n" // does not mask the formal parameter e |
207 | + " panic();\n" |
208 | + " }\n" |
209 | + " return e;\n" |
210 | + "}"))) |
211 | .withProvides("g") |
212 | .make()), |
213 | ("WARNING: testCatchBlocks:4+12 - 13:" |
214 | + " Declaration of e masks declaration at testCatchBlocks:1+12 - 13"), |
215 | "ERROR: testCatchBlocks:5+5 - 10: Symbol panic has not been defined"); |
216 | runLinterTest( |
217 | jobs(new LintJobMaker(js(fromString( |
218 | "" |
219 | + "function g(e, f) {\n" |
220 | + " try {\n" |
221 | + " f();\n" |
222 | + " } catch (e) {\n" |
223 | + " var e = f(false);\n" |
224 | + " return e + 1;\n" |
225 | + " }\n" |
226 | + " return e;\n" |
227 | + "}"))) |
228 | .withProvides("g") |
229 | .make()), |
230 | ("WARNING: testCatchBlocks:4+12 - 13:" |
231 | + " Declaration of e masks declaration at testCatchBlocks:1+12 - 13"), |
232 | ("ERROR: testCatchBlocks:5+5 - 21: e originally defined at" |
233 | + " testCatchBlocks:1+12 - 13")); |
234 | } |
235 | |
236 | public final void testLoops() throws Exception { |
237 | runLinterTest( |
238 | jobs(new LintJobMaker(js(fromString( |
239 | "" |
240 | + "(function () {\n" |
241 | + " for (var i = 0; i < 10; ++i) { f(i); }\n" |
242 | + " for (var i = 10; --i >= 0;) { f(i); }\n" |
243 | + "})();"))) |
244 | .withRequires("f").make()) |
245 | ); |
246 | runLinterTest( |
247 | jobs(new LintJobMaker(js(fromString( |
248 | "" |
249 | + "(function () {\n" |
250 | + " for (var i = 0; i < 10; ++i) {\n" |
251 | + " for (var i = 10; --i >= 0;) { f(i); }\n" |
252 | + " }\n" |
253 | + "})();"))) |
254 | .withRequires("f").make()), |
255 | ("ERROR: testLoops:3+10 - 20:" |
256 | + " Declaration of i masks declaration at testLoops:2+8 - 17")); |
257 | runLinterTest( |
258 | jobs(new LintJobMaker(js(fromString( |
259 | "" |
260 | + "(function () {\n" |
261 | + " for (var i = 0; i < 10; ++i) { f(i); }\n" |
262 | + " return i;" |
263 | + "})();"))) |
264 | .withRequires("f").make()), |
265 | ("ERROR: testLoops:3+10 - 11: Usage of i declared at " |
266 | + "testLoops:2+8 - 17 is out of block scope.") |
267 | ); |
268 | runLinterTest( |
269 | jobs(new LintJobMaker(js(fromString( |
270 | "" |
271 | + "(function (i) {\n" // variable i |
272 | + " return function (arr) {\n" |
273 | + " var sum = 0;\n" |
274 | + " for (var i = arr.length; --i >= 0;) {\n" // i masks formal |
275 | + " sum += arr[i];\n" |
276 | + " }\n" |
277 | + " return sum / i;\n" // apparent reference to a masked outer |
278 | + " };\n" |
279 | + "})(4);"))).make()), |
280 | ("ERROR: testLoops:7+18 - 19: Usage of i declared at " |
281 | + "testLoops:4+10 - 28 is out of block scope.") |
282 | ); |
283 | runLinterTest( |
284 | jobs(new LintJobMaker(js(fromString( |
285 | "" |
286 | + "(function () {\n" |
287 | + " var k;\n" |
288 | + " for (k in o);\n" |
289 | + "})();"))).make()), |
290 | "ERROR: testLoops:3+13 - 14: Symbol o has not been defined" |
291 | ); |
292 | runLinterTest( |
293 | jobs(new LintJobMaker(js(fromString("for (var k in o);"))).make()), |
294 | "ERROR: testLoops:1+15 - 16: Symbol o has not been defined", |
295 | "LINT: testLoops:1+6 - 11: Undocumented global k"); |
296 | } |
297 | |
298 | public final void testIgnoredValue() throws Exception { |
299 | runLinterTest( |
300 | jobs(new LintJobMaker(js(fromString( |
301 | "" |
302 | + "var c; f; \n" // Line 1 |
303 | + "+n; \n" // line 2 |
304 | + "a.b; \n" // line 3 |
305 | + "f && g(); f && g; f() && g; \n" // line 4: 1st OK, others not |
306 | + "a = b, c = d; \n" // line 5 |
307 | + "a = b; \n" // OK |
308 | + "m += n; \n" // OK |
309 | + "f(); \n" // OK |
310 | + "new Array; \n" // line 9 |
311 | + "new Array(); \n" // line 10 |
312 | + "for (a = b, c = d; !a; ++a, --m, ++c) f; \n" // line 11 |
313 | + "++c; \n" // OK |
314 | + "while (1) { 1; }\n" // line 13. First allowed, second not |
315 | + "({ x: 32 });\n" |
316 | // 1 2 3 4 |
317 | // 1234567890123456789012345678901234567890 |
318 | ))) |
319 | .withRequires("b", "d", "f", "g", "n") |
320 | .withOverrides("a", "m") |
321 | .withProvides("c") |
322 | .make()), |
323 | "WARNING: testIgnoredValue:1+8 - 9: Operation has no effect", |
324 | "WARNING: testIgnoredValue:2+1 - 3: Operation has no effect", |
325 | "WARNING: testIgnoredValue:3+1 - 4: Operation has no effect", |
326 | "WARNING: testIgnoredValue:4+12 - 18: Operation has no effect", |
327 | "WARNING: testIgnoredValue:4+21 - 29: Operation has no effect", |
328 | "WARNING: testIgnoredValue:5+1 - 13: Operation has no effect", |
329 | "WARNING: testIgnoredValue:9+1 - 10: Operation has no effect", |
330 | "WARNING: testIgnoredValue:10+1 - 12: Operation has no effect", |
331 | "WARNING: testIgnoredValue:11+39 - 40: Operation has no effect", |
332 | "WARNING: testIgnoredValue:13+13 - 14: Operation has no effect", |
333 | "WARNING: testIgnoredValue:14+1 - 12: Operation has no effect"); |
334 | } |
335 | |
336 | public final void testDeadCode() throws Exception { |
337 | runLinterTest( |
338 | jobs(new LintJobMaker(js(fromString( |
339 | "" |
340 | + "(function () {\n" |
341 | + " return\n" |
342 | + " foo();\n" |
343 | + "})();"))) |
344 | .withRequires("foo") |
345 | .make()), |
346 | // runLinterTest makes its own message queue so the lint message about |
347 | // semicolons does not show here. |
348 | // Linter's main method does use the same message queue though, so |
349 | // parsing messages will be reported. |
350 | "WARNING: testDeadCode:3+7 - 12: Code is not reachable"); |
351 | runLinterTest( |
352 | jobs(new LintJobMaker(js(fromString( |
353 | "" |
354 | + "with (o) {\n" |
355 | // Is reachable even though code within a with block is not |
356 | // analyzable. Ignore reachability in with blocks. |
357 | + " foo();\n" |
358 | + "}\n"))) |
359 | .withRequires("o", "foo") |
360 | .make()) |
361 | ); |
362 | } |
363 | |
364 | public final void testLiveness() throws Exception { |
365 | runLinterTest( |
366 | jobs(new LintJobMaker(js(fromString( |
367 | "" |
368 | + "var a;\n" |
369 | + "if (Math.random()) {\n" |
370 | + " a = new Error();\n" |
371 | + "}\n" |
372 | + "throw a;\n" |
373 | ))) |
374 | .withProvides("a") |
375 | .make()), |
376 | ("WARNING: testLiveness:5+7 - 8: Symbol a may be used before" |
377 | + " being initialized"), |
378 | "ERROR: testLiveness: @provides a not provided", |
379 | ("WARNING: testLiveness:5+1 - 8:" |
380 | + " Uncaught exception thrown during initialization") |
381 | ); |
382 | } |
383 | |
384 | public final void testLabels() throws Exception { |
385 | runLinterTest( |
386 | jobs(new LintJobMaker(js(fromString( |
387 | "foo: for (;;) { foo: for (;;); }"))).make()), |
388 | ("ERROR: testLabels:1+17 - 31:" |
389 | + " Label foo nested inside testLabels:1+1 - 33")); |
390 | // Side-by-side is fine. |
391 | runLinterTest( |
392 | jobs(new LintJobMaker(js(fromString( |
393 | "foo: for (;;); foo: for (;;);"))).make())); |
394 | // And it's ok if they have the default label |
395 | runLinterTest( |
396 | jobs(new LintJobMaker(js(fromString( |
397 | "for (;;) { for (;;); }"))).make())); |
398 | // or different labels |
399 | runLinterTest( |
400 | jobs(new LintJobMaker(js(fromString( |
401 | "foo: for (;;) { bar: for (;;); }"))).make())); |
402 | } |
403 | |
404 | public final void testMisplacedExits() throws Exception { |
405 | runLinterTest( |
406 | jobs(new LintJobMaker(js(fromString("return;"))).make()), |
407 | ("ERROR: testMisplacedExits:1+1 - 7:" |
408 | + " Return does not appear inside a function")); |
409 | runLinterTest( |
410 | jobs(new LintJobMaker(js(fromString( |
411 | "foo: for (;;) { break bar; }" |
412 | ))).make()), |
413 | ("ERROR: testMisplacedExits:1+17 - 26:" |
414 | + " Unmatched break or continue to label bar")); |
415 | runLinterTest( |
416 | jobs(new LintJobMaker(js(fromString( |
417 | "switch (Math.random()) { case 0: continue; }" |
418 | ))).make()), |
419 | ("ERROR: testMisplacedExits:1+34 - 42:" |
420 | + " Unmatched break or continue to label <default>")); |
421 | } |
422 | |
423 | public final void testIEQuirksScoping() throws Exception { |
424 | runLinterTest( |
425 | jobs(new LintJobMaker(js(fromString( |
426 | "" |
427 | + "var myObject = {\n" |
428 | + " foo: function foo() {}\n" |
429 | + "};\n" |
430 | ))) |
431 | .withProvides("myObject") |
432 | .make()), |
433 | "LINT: testIEQuirksScoping:2+10 - 27: Undocumented global foo"); |
434 | // No problem if done inside a closure. |
435 | runLinterTest( |
436 | jobs(new LintJobMaker(js(fromString( |
437 | "" |
438 | + "var myObject = (function () {\n" |
439 | + " return {\n" |
440 | + " foo: function foo() {}\n" |
441 | + " };\n" |
442 | + "})();" |
443 | ))) |
444 | .withProvides("myObject") |
445 | .make())); |
446 | // Unless doing so would conflict with a local variable. |
447 | // Even if the variable isn't used later, it could still break recursion |
448 | // within the function. |
449 | runLinterTest( |
450 | jobs(new LintJobMaker(js(fromString( |
451 | "" |
452 | + "var myObject = (function () {\n" |
453 | + " var foo = {\n" |
454 | + " foo: function foo() {}\n" |
455 | + " };\n" |
456 | + " return foo;\n" |
457 | + "})();" |
458 | ))) |
459 | .withProvides("myObject") |
460 | .make()), |
461 | ("ERROR: testIEQuirksScoping:3+12 - 29: foo originally defined" |
462 | + " at testIEQuirksScoping:2+3 - 4+4")); |
463 | // And we report a slightly different message when there's a block scope in |
464 | // between. |
465 | runLinterTest( |
466 | jobs(new LintJobMaker(js(fromString( |
467 | "" |
468 | + "var myObject = (function () {\n" |
469 | + " var foo;\n" |
470 | + " do \n" |
471 | + " foo = {\n" |
472 | + " foo: function foo() {}\n" |
473 | + " };\n" |
474 | + " while (false);\n" |
475 | + " return foo;\n" |
476 | + "})();" |
477 | ))) |
478 | .withProvides("myObject") |
479 | .make()), |
480 | ("ERROR: testIEQuirksScoping:5+14 - 31: foo originally defined" |
481 | + " at testIEQuirksScoping:2+3 - 10")); |
482 | } |
483 | |
484 | // TODO(mikesamuel): |
485 | // check that function bodies either never return or never complete. |
486 | // E.g. function f(x, y) { if (x) { return y; } } is missing an else. |
487 | |
488 | final static class LintJobMaker { |
489 | private Block node; |
490 | private Set<String> provides = Sets.newLinkedHashSet(), |
491 | requires = Sets.newLinkedHashSet(), |
492 | overrides = Sets.newLinkedHashSet(); |
493 | LintJobMaker(Block node) { |
494 | this.node = node; |
495 | } |
496 | |
497 | LintJobMaker withProvides(String... idents) { |
498 | provides.addAll(Arrays.asList(idents)); |
499 | return this; |
500 | } |
501 | |
502 | LintJobMaker withRequires(String... idents) { |
503 | requires.addAll(Arrays.asList(idents)); |
504 | return this; |
505 | } |
506 | |
507 | LintJobMaker withOverrides(String... idents) { |
508 | overrides.addAll(Arrays.asList(idents)); |
509 | return this; |
510 | } |
511 | |
512 | Linter.LintJob make() { |
513 | return new Linter.LintJob( |
514 | node.getFilePosition().source(), requires, provides, overrides, node); |
515 | } |
516 | } |
517 | |
518 | private void runLinterTest(List<Linter.LintJob> inputs, String... messages) { |
519 | MessageQueue mq = new SimpleMessageQueue(); |
520 | Linter.lint(inputs, new Linter.Environment(Sets.<String>newHashSet()), mq); |
521 | List<String> actualMessageStrs = Lists.newArrayList(); |
522 | for (Message msg : mq.getMessages()) { |
523 | actualMessageStrs.add( |
524 | msg.getMessageLevel().name() + ": " + msg.format(mc)); |
525 | } |
526 | |
527 | List<String> goldenMessageStrs = Lists.newArrayList(messages); |
528 | Collections.sort(actualMessageStrs); |
529 | Collections.sort(goldenMessageStrs); |
530 | MoreAsserts.assertListsEqual(goldenMessageStrs, actualMessageStrs); |
531 | } |
532 | |
533 | private List<Linter.LintJob> jobs(Linter.LintJob... jobs) { |
534 | return Arrays.asList(jobs); |
535 | } |
536 | } |