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 }