|
1 /* |
|
2 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. |
|
3 * |
|
4 * Redistribution and use in source and binary forms, with or without |
|
5 * modification, are permitted provided that the following conditions |
|
6 * are met: |
|
7 * |
|
8 * - Redistributions of source code must retain the above copyright |
|
9 * notice, this list of conditions and the following disclaimer. |
|
10 * |
|
11 * - Redistributions in binary form must reproduce the above copyright |
|
12 * notice, this list of conditions and the following disclaimer in the |
|
13 * documentation and/or other materials provided with the distribution. |
|
14 * |
|
15 * - Neither the name of Oracle nor the names of its |
|
16 * contributors may be used to endorse or promote products derived |
|
17 * from this software without specific prior written permission. |
|
18 * |
|
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
|
20 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
|
21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
22 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
|
23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|
24 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|
25 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|
26 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|
27 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|
28 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
29 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
30 */ |
|
31 |
|
32 // Usage: jjs --language=es6 staticchecker.js -- <file> |
|
33 // or jjs --language=es6 staticchecker.js -- <directory> |
|
34 // default argument is the current directory |
|
35 |
|
36 if (arguments.length == 0) { |
|
37 arguments[0] = "."; |
|
38 } |
|
39 |
|
40 const File = Java.type("java.io.File"); |
|
41 const file = new File(arguments[0]); |
|
42 if (!file.exists()) { |
|
43 print(arguments[0] + " is neither a file nor a directory"); |
|
44 exit(1); |
|
45 } |
|
46 |
|
47 // A simple static checker for javascript best practices. |
|
48 // static checks performed are: |
|
49 // |
|
50 // * __proto__ magic property is bad (non-standard) |
|
51 // * 'with' statements are bad |
|
52 // * 'eval' calls are bad |
|
53 // * 'delete foo' (scope variable delete) is bad |
|
54 // * assignment to standard globals is bad (eg. Object = "hello") |
|
55 // * assignment to property on standard prototype is bad (eg. String.prototype.foo = 45) |
|
56 // * exception swallow (empty catch block in try-catch statements) |
|
57 |
|
58 const Files = Java.type("java.nio.file.Files"); |
|
59 const EmptyStatementTree = Java.type("jdk.nashorn.api.tree.EmptyStatementTree"); |
|
60 const IdentifierTree = Java.type("jdk.nashorn.api.tree.IdentifierTree"); |
|
61 const MemberSelectTree = Java.type("jdk.nashorn.api.tree.MemberSelectTree"); |
|
62 const Parser = Java.type("jdk.nashorn.api.tree.Parser"); |
|
63 const SimpleTreeVisitor = Java.type("jdk.nashorn.api.tree.SimpleTreeVisitorES6"); |
|
64 const Tree = Java.type("jdk.nashorn.api.tree.Tree"); |
|
65 |
|
66 const parser = Parser.create("-scripting", "--language=es6"); |
|
67 |
|
68 // capture standard global upfront |
|
69 const globals = new Set(); |
|
70 for (let name of Object.getOwnPropertyNames(this)) { |
|
71 globals.add(name); |
|
72 } |
|
73 |
|
74 const checkFile = function(file) { |
|
75 print("Parsing " + file); |
|
76 const ast = parser.parse(file, print); |
|
77 if (!ast) { |
|
78 print("FAILED to parse: " + file); |
|
79 return; |
|
80 } |
|
81 |
|
82 const checker = new (Java.extend(SimpleTreeVisitor)) { |
|
83 lineMap: null, |
|
84 |
|
85 printWarning(node, msg) { |
|
86 var pos = node.startPosition; |
|
87 var line = this.lineMap.getLineNumber(pos); |
|
88 var column = this.lineMap.getColumnNumber(pos); |
|
89 print(`WARNING: ${msg} in ${file} @ ${line}:${column}`); |
|
90 }, |
|
91 |
|
92 printWithWarning(node) { |
|
93 this.printWarning(node, "'with' usage"); |
|
94 }, |
|
95 |
|
96 printProtoWarning(node) { |
|
97 this.printWarning(node, "__proto__ usage"); |
|
98 }, |
|
99 |
|
100 printScopeDeleteWarning(node, varName) { |
|
101 this.printWarning(node, `delete ${varName}`); |
|
102 }, |
|
103 |
|
104 hasOnlyEmptyStats(stats) { |
|
105 const itr = stats.iterator(); |
|
106 while (itr.hasNext()) { |
|
107 if (! (itr.next() instanceof EmptyStatementTree)) { |
|
108 return false; |
|
109 } |
|
110 } |
|
111 |
|
112 return true; |
|
113 }, |
|
114 |
|
115 checkProto(node, name) { |
|
116 if (name == "__proto__") { |
|
117 this.printProtoWarning(node); |
|
118 } |
|
119 }, |
|
120 |
|
121 checkAssignment(lhs) { |
|
122 if (lhs instanceof IdentifierTree && globals.has(lhs.name)) { |
|
123 this.printWarning(lhs, `assignment to standard global "${lhs.name}"`); |
|
124 } else if (lhs instanceof MemberSelectTree) { |
|
125 const expr = lhs.expression; |
|
126 if (expr instanceof MemberSelectTree && |
|
127 expr.expression instanceof IdentifierTree && |
|
128 globals.has(expr.expression.name) && |
|
129 "prototype" == expr.identifier) { |
|
130 this.printWarning(lhs, |
|
131 `property set "${expr.expression.name}.prototype.${lhs.identifier}"`); |
|
132 } |
|
133 } |
|
134 }, |
|
135 |
|
136 visitAssignment(node, extra) { |
|
137 this.checkAssignment(node.variable); |
|
138 Java.super(checker).visitAssignment(node, extra); |
|
139 }, |
|
140 |
|
141 visitCatch(node, extra) { |
|
142 var stats = node.block.statements; |
|
143 if (stats.empty || this.hasOnlyEmptyStats(stats)) { |
|
144 this.printWarning(node, "exception swallow"); |
|
145 } |
|
146 Java.super(checker).visitCatch(node, extra); |
|
147 }, |
|
148 |
|
149 visitCompilationUnit(node, extra) { |
|
150 this.lineMap = node.lineMap; |
|
151 Java.super(checker).visitCompilationUnit(node, extra); |
|
152 }, |
|
153 |
|
154 visitFunctionCall(node, extra) { |
|
155 var func = node.functionSelect; |
|
156 if (func instanceof IdentifierTree && func.name == "eval") { |
|
157 this.printWarning(node, "eval call found"); |
|
158 } |
|
159 Java.super(checker).visitFunctionCall(node, extra); |
|
160 }, |
|
161 |
|
162 visitIdentifier(node, extra) { |
|
163 this.checkProto(node, node.name); |
|
164 Java.super(checker).visitIdentifier(node, extra); |
|
165 }, |
|
166 |
|
167 visitMemberSelect(node, extra) { |
|
168 this.checkProto(node, node.identifier); |
|
169 Java.super(checker).visitMemberSelect(node, extra); |
|
170 }, |
|
171 |
|
172 visitProperty(node, extra) { |
|
173 this.checkProto(node, node.key); |
|
174 Java.super(checker).visitProperty(node, extra); |
|
175 }, |
|
176 |
|
177 visitUnary(node, extra) { |
|
178 if (node.kind == Tree.Kind.DELETE && |
|
179 node.expression instanceof IdentifierTree) { |
|
180 this.printScopeDeleteWarning(node, node.expression.name); |
|
181 } |
|
182 Java.super(checker).visitUnary(node, extra); |
|
183 }, |
|
184 |
|
185 visitWith(node, extra) { |
|
186 this.printWithWarning(node); |
|
187 Java.super(checker).visitWith(node, extra); |
|
188 } |
|
189 }; |
|
190 |
|
191 try { |
|
192 ast.accept(checker, null); |
|
193 } catch (e) { |
|
194 print(e); |
|
195 if (e.printStackTrace) e.printStackTrace(); |
|
196 if (e.stack) print(e.stack); |
|
197 } |
|
198 } |
|
199 |
|
200 if (file.isDirectory()) { |
|
201 Files.walk(file.toPath()) |
|
202 .filter(function(p) Files.isRegularFile(p)) |
|
203 .filter(function(p) p.toFile().name.endsWith('.js')) |
|
204 .forEach(checkFile); |
|
205 } else { |
|
206 checkFile(file); |
|
207 } |