src/sample/nashorn/staticchecker.js
author herrick
Tue, 12 Feb 2019 11:54:56 -0500
branchJDK-8200758-branch
changeset 57180 1d6f8a7c6d5a
parent 47457 217860329f71
permissions -rw-r--r--
8217687: jpackage --help improvements Reviewed-by: almatvee

/*
 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Oracle nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// Usage: jjs --language=es6 staticchecker.js -- <file>
//    or  jjs --language=es6 staticchecker.js -- <directory>
// default argument is the current directory

if (arguments.length == 0) {
    arguments[0] = ".";
}

const File = Java.type("java.io.File");
const file = new File(arguments[0]);
if (!file.exists()) {
    print(arguments[0] + " is neither a file nor a directory");
    exit(1);
}

// A simple static checker for javascript best practices.
// static checks performed are:
//
// *  __proto__ magic property is bad (non-standard)
// * 'with' statements are bad
// * 'eval' calls are bad
// * 'delete foo' (scope variable delete) is bad
// * assignment to standard globals is bad (eg. Object = "hello")
// * assignment to property on standard prototype is bad (eg. String.prototype.foo = 45)
// * exception swallow (empty catch block in try-catch statements)

const Files = Java.type("java.nio.file.Files");
const EmptyStatementTree = Java.type("jdk.nashorn.api.tree.EmptyStatementTree");
const IdentifierTree = Java.type("jdk.nashorn.api.tree.IdentifierTree");
const MemberSelectTree = Java.type("jdk.nashorn.api.tree.MemberSelectTree");
const Parser = Java.type("jdk.nashorn.api.tree.Parser");
const SimpleTreeVisitor = Java.type("jdk.nashorn.api.tree.SimpleTreeVisitorES6");
const Tree = Java.type("jdk.nashorn.api.tree.Tree");

const parser = Parser.create("-scripting", "--language=es6");

// capture standard global upfront
const globals = new Set();
for (let name of Object.getOwnPropertyNames(this)) {
    globals.add(name);
}

const checkFile = function(file) {
    print("Parsing " + file);
    const ast = parser.parse(file, print);
    if (!ast) {
        print("FAILED to parse: " + file);
        return;
    }

    const checker = new (Java.extend(SimpleTreeVisitor)) {
        lineMap: null,

        printWarning(node, msg) {
            var pos = node.startPosition;
            var line = this.lineMap.getLineNumber(pos);
            var column = this.lineMap.getColumnNumber(pos);
            print(`WARNING: ${msg} in ${file} @ ${line}:${column}`);
        },
        
        printWithWarning(node) {
            this.printWarning(node, "'with' usage");
        },

        printProtoWarning(node) {
            this.printWarning(node, "__proto__ usage");
        },

        printScopeDeleteWarning(node, varName) {
            this.printWarning(node, `delete ${varName}`);
        },

        hasOnlyEmptyStats(stats) {
            const itr = stats.iterator();
            while (itr.hasNext()) {
                if (! (itr.next() instanceof EmptyStatementTree)) {
                    return false;
                }
            }

            return true;
        },

        checkProto(node, name) {
            if (name == "__proto__") {
                this.printProtoWarning(node);
            }
        },

        checkAssignment(lhs) {
            if (lhs instanceof IdentifierTree && globals.has(lhs.name)) {
                this.printWarning(lhs, `assignment to standard global "${lhs.name}"`);
            } else if (lhs instanceof MemberSelectTree) {
                const expr = lhs.expression;
                if (expr instanceof MemberSelectTree &&
                    expr.expression instanceof IdentifierTree &&
                    globals.has(expr.expression.name) && 
                    "prototype" == expr.identifier) {
                    this.printWarning(lhs, 
                        `property set "${expr.expression.name}.prototype.${lhs.identifier}"`);
                }
            }
        },

        visitAssignment(node, extra) {
            this.checkAssignment(node.variable);
            Java.super(checker).visitAssignment(node, extra);
        },

        visitCatch(node, extra) {
            var stats = node.block.statements;
            if (stats.empty || this.hasOnlyEmptyStats(stats)) {
                this.printWarning(node, "exception swallow");
            }
            Java.super(checker).visitCatch(node, extra);
        },

        visitCompilationUnit(node, extra) {
            this.lineMap = node.lineMap;
            Java.super(checker).visitCompilationUnit(node, extra);
        },

        visitFunctionCall(node, extra) {
           var func = node.functionSelect;
           if (func instanceof IdentifierTree && func.name == "eval") {
               this.printWarning(node, "eval call found");
           }
           Java.super(checker).visitFunctionCall(node, extra);
        },

        visitIdentifier(node, extra) {
            this.checkProto(node, node.name);
            Java.super(checker).visitIdentifier(node, extra);
        },

        visitMemberSelect(node, extra) {
            this.checkProto(node, node.identifier);
            Java.super(checker).visitMemberSelect(node, extra);
        },

        visitProperty(node, extra) {
            this.checkProto(node, node.key);
            Java.super(checker).visitProperty(node, extra);
        },

        visitUnary(node, extra) {
            if (node.kind == Tree.Kind.DELETE &&
                node.expression instanceof IdentifierTree) {
                this.printScopeDeleteWarning(node, node.expression.name);
            }
            Java.super(checker).visitUnary(node, extra);
        },

        visitWith(node, extra) {
            this.printWithWarning(node);
            Java.super(checker).visitWith(node, extra);
        }
    };

    try {
        ast.accept(checker, null);
    } catch (e) {
        print(e);
        if (e.printStackTrace) e.printStackTrace();
        if (e.stack) print(e.stack);
    }
}

if (file.isDirectory()) {
    Files.walk(file.toPath())
        .filter(function(p) Files.isRegularFile(p))
        .filter(function(p) p.toFile().name.endsWith('.js'))
        .forEach(checkFile);
} else {
    checkFile(file);
}