com.google.caja.ancillary.linter.Linter.java Source code

Java tutorial

Introduction

Here is the source code for com.google.caja.ancillary.linter.Linter.java

Source

// Copyright (C) 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.caja.ancillary.linter;

import com.google.caja.lexer.CharProducer;
import com.google.caja.lexer.FilePosition;
import com.google.caja.lexer.InputSource;
import com.google.caja.lexer.JsLexer;
import com.google.caja.lexer.JsTokenQueue;
import com.google.caja.lexer.Keyword;
import com.google.caja.lexer.ParseException;
import com.google.caja.lexer.Token;
import com.google.caja.lexer.TokenConsumer;
import com.google.caja.parser.AncestorChain;
import com.google.caja.parser.ParseTreeNode;
import com.google.caja.parser.ParserBase;
import com.google.caja.parser.js.ArrayConstructor;
import com.google.caja.parser.js.Block;
import com.google.caja.parser.js.BreakStmt;
import com.google.caja.parser.js.CatchStmt;
import com.google.caja.parser.js.ContinueStmt;
import com.google.caja.parser.js.Expression;
import com.google.caja.parser.js.ExpressionStmt;
import com.google.caja.parser.js.ForEachLoop;
import com.google.caja.parser.js.ForLoop;
import com.google.caja.parser.js.FunctionConstructor;
import com.google.caja.parser.js.FunctionDeclaration;
import com.google.caja.parser.js.LabeledStatement;
import com.google.caja.parser.js.Literal;
import com.google.caja.parser.js.Loop;
import com.google.caja.parser.js.ObjProperty;
import com.google.caja.parser.js.ObjectConstructor;
import com.google.caja.parser.js.Operation;
import com.google.caja.parser.js.Operator;
import com.google.caja.parser.js.OperatorCategory;
import com.google.caja.parser.js.Parser;
import com.google.caja.parser.js.Reference;
import com.google.caja.parser.js.ReturnStmt;
import com.google.caja.parser.js.Statement;
import com.google.caja.parser.js.StringLiteral;
import com.google.caja.parser.js.ThrowStmt;
import com.google.caja.parser.js.WithStmt;
import com.google.caja.reporting.Message;
import com.google.caja.reporting.MessageContext;
import com.google.caja.reporting.MessageLevel;
import com.google.caja.reporting.MessagePart;
import com.google.caja.reporting.MessageQueue;
import com.google.caja.reporting.MessageType;
import com.google.caja.reporting.RenderContext;
import com.google.caja.reporting.SimpleMessageQueue;
import com.google.caja.tools.BuildCommand;
import com.google.caja.util.Charsets;
import com.google.caja.util.Pair;
import com.google.caja.util.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

/**
 * A build task that performs sanity checks on JavaScript inputs, and if there
 * are no warnings or errors, outputs a time-stamp file to record the time at
 * which the linter passed.
 *
 * @author mikesamuel@gmail.com
 */
public class Linter implements BuildCommand {
    private final Environment env;

    public Linter() {
        this(new Environment(Sets.<String>newHashSet()));
    }

    public Linter(Environment env) {
        this.env = env;
    }

    public boolean build(List<File> inputs, List<File> dependencies, Map<String, Object> options, File output)
            throws IOException {
        Set<?> ignores = (Set<?>) options.get("toIgnore");
        if (ignores == null) {
            ignores = Collections.emptySet();
        }
        MessageContext mc = new MessageContext();
        Map<InputSource, CharSequence> contentMap = Maps.newLinkedHashMap();
        MessageQueue mq = new SimpleMessageQueue();
        List<LintJob> lintJobs = parseInputs(inputs, contentMap, mc, mq);
        lint(lintJobs, env, mq);
        if (!ignores.isEmpty()) {
            for (Iterator<Message> it = mq.getMessages().iterator(); it.hasNext();) {
                if (ignores.contains(it.next().getMessageType().name())) {
                    it.remove();
                }
            }
        }
        if (output.getName().endsWith(".stamp") || output.getName().endsWith(".tstamp")) {
            if (ErrorReporter.reportErrors(contentMap, mc, mq, System.out).compareTo(MessageLevel.WARNING) < 0) {
                // Touch the time-stamp file to make it clear that the inputs were
                // successfully linted.
                (new FileOutputStream(output)).close();
                return true;
            } else {
                return false;
            }
        } else {
            Writer reportOut = new OutputStreamWriter(new FileOutputStream(output), Charsets.UTF_8);
            boolean result;
            try {
                result = ErrorReporter.reportErrors(contentMap, mc, mq, reportOut)
                        .compareTo(MessageLevel.WARNING) < 0;
            } finally {
                try {
                    reportOut.close();
                } catch (IOException ex) {
                    result = false;
                }
            }
            return result;
        }
    }

    private static List<LintJob> parseInputs(List<File> inputs, Map<InputSource, CharSequence> contents,
            MessageContext mc, MessageQueue mq) throws IOException {
        List<LintJob> compUnits = Lists.newArrayList();
        // Parse each input, and find annotations.
        for (File inp : inputs) {
            CharProducer cp = CharProducer.Factory.fromFile(inp, "UTF-8");
            if (cp.isEmpty()) {
                continue;
            }

            InputSource src = cp.getCurrentPosition().source();
            mc.addInputSource(src);
            contents.put(src, new FileContent(cp));

            JsTokenQueue tq = new JsTokenQueue(new JsLexer(cp), src);
            try {
                if (tq.isEmpty()) {
                    continue;
                }
                Parser p = new Parser(tq, mq);
                compUnits.add(makeLintJob(p.parse(), mq));
            } catch (ParseException ex) {
                ex.toMessageQueue(mq);
            }
        }
        return compUnits;
    }

    public static LintJob makeLintJob(Block program, MessageQueue mq) {
        InputSource src = program.getFilePosition().source();
        List<Token<?>> tokens = program.getComments();
        return new LintJob(src, parseIdentifierListFromComment("requires", tokens, mq),
                parseIdentifierListFromComment("provides", tokens, mq),
                parseIdentifierListFromComment("overrides", tokens, mq), program);
    }

    public static void lint(List<LintJob> jobs, Environment env, MessageQueue mq) {
        for (LintJob job : jobs) {
            lint(AncestorChain.instance(job.program), env,
                    // Anything defined by this file can be read by this file.
                    job.provides, job.requires, job.overrides, mq);
        }
        // Check that two files do not provide the same thing.
        Map<String, InputSource> providedBy = Maps.newHashMap();
        for (LintJob job : jobs) {
            for (String symbolName : job.provides) {
                InputSource originallyDefinedIn = providedBy.put(symbolName, job.src);
                if (originallyDefinedIn != null) {
                    mq.addMessage(LinterMessageType.MULTIPLY_PROVIDED_SYMBOL, job.src, originallyDefinedIn,
                            MessagePart.Factory.valueOf(symbolName));
                }
            }
        }
    }

    /**
     * @param ac the node to check.
     * @param mq receives messages about violations of canRead and canSet.
     */
    private static void lint(AncestorChain<?> ac, final Environment env, Set<String> provides,
            final Set<String> requires, final Set<String> overrides, MessageQueue mq) {
        ScopeAnalyzer sa = new ScopeAnalyzer() {
            @Override
            protected boolean introducesScope(AncestorChain<?> ac) {
                if (super.introducesScope(ac)) {
                    return true;
                }
                return isLoopy(ac);
            }

            @Override
            protected void initScope(LexicalScope scope) {
                super.initScope(scope);
                if (scope.isFunctionScope()) {
                    FunctionConstructor fc = scope.root.cast(FunctionConstructor.class).node;
                    // Simulate JScript quirks around named functions
                    if (fc.getIdentifierName() != null && scope.root.parent != null
                            && !(scope.root.parent.node instanceof FunctionDeclaration)) {
                        LexicalScope containing = scope.parent;
                        while (containing.parent != null
                                && (hoist(scope.root, containing) || isLoopy(containing.root))) {
                            containing = containing.parent;
                        }
                        containing.symbols.declare(fc.getIdentifierName(), scope.root);
                    }
                } else if (scope.isGlobal()) {
                    for (String symbolName : Sets.union(env.outers, Sets.union(requires, overrides))) {
                        if (scope.symbols.getSymbol(symbolName) == null) {
                            scope.symbols.declare(symbolName, scope.root);
                        }
                    }
                }
            }

            boolean isLoopy(AncestorChain<?> ac) {
                ParseTreeNode node = ac.node;
                return node instanceof ForEachLoop || node instanceof Loop;
            }
        };
        List<LexicalScope> scopes = sa.computeLexicalScopes(ac);
        LexicalScope globalScope = scopes.get(0);
        VariableLiveness.LiveCalc lc = VariableLiveness.calculateLiveness(ac.node);
        NodeBuckets buckets = NodeBuckets.maker().with(ExpressionStmt.class).with(LabeledStatement.class)
                .with(StringLiteral.class).under(globalScope.root);

        checkDeclarations(scopes, overrides, mq);
        checkLabels(lc, buckets, mq);
        checkUses(scopes, lc.vars, provides, requires, overrides, mq);
        checkSideEffects(buckets, mq);
        checkDeadCode(buckets, mq);
        checkStringsEmbeddable((Block) ac.node, buckets, mq);
        checkBareWords(globalScope.root.node, mq);
    }

    private static void checkBareWords(ParseTreeNode node, MessageQueue mq) {
        if (node instanceof ObjProperty) {
            ObjProperty op = (ObjProperty) node;
            String key = op.getPropertyNameNode().getValue();
            if (Keyword.isKeyword(key)) {
                mq.addMessage(LinterMessageType.BARE_KEYWORD, op.getPropertyNameNode().getFilePosition(),
                        MessagePart.Factory.valueOf(key));
            }
        }
        if (Operation.is(node, Operator.MEMBER_ACCESS)) {
            ParseTreeNode right = node.children().get(1);
            if (right instanceof Reference) {
                String key = ((Reference) right).getIdentifierName();
                if (Keyword.isKeyword(key)) {
                    mq.addMessage(LinterMessageType.BARE_KEYWORD, right.getFilePosition(),
                            MessagePart.Factory.valueOf(key));
                }
            }
        }
        for (ParseTreeNode child : node.children()) {
            checkBareWords(child, mq);
        }
    }

    private static void checkDeclarations(List<LexicalScope> scopes, Set<String> overrides, MessageQueue mq) {
        // Check that declarations don't conflict
        for (LexicalScope scope : scopes) {
            for (String symbolName : scope.symbols.symbolNames()) {
                Collection<AncestorChain<?>> declarations = scope.symbols.getSymbol(symbolName).getDeclarations();
                if (declarations.size() != 1) {
                    Iterator<AncestorChain<?>> it = declarations.iterator();
                    AncestorChain<?> original = it.next();
                    // Overrides are weird.  They may already exist, but it's often best
                    // to define them.  But there's no reason to redefine built-ins.
                    if (!(scope.isGlobal() && overrides.contains(symbolName))) {
                        while (it.hasNext()) {
                            AncestorChain<?> redefinition = it.next();
                            mq.addMessage(MessageType.SYMBOL_REDEFINED, redefinition.node.getFilePosition(),
                                    MessagePart.Factory.valueOf(symbolName), original.node.getFilePosition());
                        }
                    }
                }
                // Check that this symbol does not mask one in the same function scope.
                if (!scope.isFunctionScope()) {
                    for (LexicalScope p = scope; (p = p.parent) != null;) {
                        SymbolTable.Symbol masked = p.symbols.getSymbol(symbolName);
                        if (masked != null) {
                            AncestorChain<?> firstMasked = masked.getDeclarations().iterator().next();
                            mq.addMessage(MessageType.MASKING_SYMBOL,
                                    (scope.isCatchScope() ? MessageLevel.WARNING : MessageLevel.ERROR),
                                    declarations.iterator().next().node.getFilePosition(),
                                    MessagePart.Factory.valueOf(symbolName), firstMasked.node.getFilePosition());
                        }
                        if (p.isFunctionScope()) {
                            break;
                        }
                    }
                }
            }
        }
    }

    private static void checkLabels(VariableLiveness.LiveCalc lc, NodeBuckets buckets, MessageQueue mq) {
        // Complain about break/continues to non-existent labels.
        for (Statement exit : lc.exits.liveExits()) {
            if (exit instanceof BreakStmt || exit instanceof ContinueStmt) {
                String label = (String) exit.getValue();
                if ("".equals(label)) {
                    label = "<default>";
                }
                mq.addMessage(LinterMessageType.LABEL_DOES_NOT_MATCH_LOOP, exit.getFilePosition(),
                        MessagePart.Factory.valueOf(label));
            } else if (exit instanceof ReturnStmt) {
                mq.addMessage(LinterMessageType.RETURN_OUTSIDE_FUNCTION, exit.getFilePosition());
            } else if (exit instanceof ThrowStmt) {
                mq.addMessage(LinterMessageType.UNCAUGHT_THROW_DURING_INIT, exit.getFilePosition());
            }
        }

        // Complain about masking labels
        for (AncestorChain<LabeledStatement> ls : buckets.get(LabeledStatement.class)) {
            String label = ls.node.getLabel();
            if ("".equals(label)) {
                continue;
            } // allowed to nest
            for (AncestorChain<?> p = ls; (p = p.parent) != null;) {
                if (p.node instanceof LabeledStatement
                        && label.equals(p.cast(LabeledStatement.class).node.getLabel())) {
                    mq.addMessage(LinterMessageType.DUPLICATE_LABEL, ls.node.getFilePosition(),
                            MessagePart.Factory.valueOf(label), p.node.getFilePosition());
                    break; // Since we have already seen p in buckets
                }
            }
        }
    }

    private static void checkUses(List<LexicalScope> scopes, LiveSet liveAtEnd, Set<String> provides,
            Set<String> requires, Set<String> overrides, MessageQueue mq) {
        LexicalScope globalScope = scopes.get(0);
        // Symbols banned since they are unsafe due to differences between
        // expectations of block scoping and actual ES scoping rules.
        Map<Pair<LexicalScope, String>, LexicalScope> banned = Maps.newHashMap();
        for (LexicalScope scope : scopes) {
            if (scope.isFunctionScope() || scope.isCatchScope() || scope.isWithScope()) {
                continue;
            }
            for (LexicalScope p = scope; !p.isFunctionScope() && (p = p.parent) != null;) {
                for (String symbolName : scope.symbols.symbolNames()) {
                    Pair<LexicalScope, String> symbol = Pair.pair(p, symbolName);
                    if (!banned.containsKey(symbol) && p.symbols.getSymbol(symbolName) == null) {
                        banned.put(symbol, scope);
                    }
                }
            }
        }

        // Check that uses are consistent with declarations
        Set<String> undeclaredGlobals = Sets.newHashSet();
        Map<String, ScopeAnalyzer.Use> globalsRead = Maps.newHashMap();
        Map<String, ScopeAnalyzer.Use> globalsSet = Maps.newHashMap();
        Map<String, ScopeAnalyzer.Use> globalsModified = Maps.newHashMap();
        for (ScopeAnalyzer.Use use : ScopeAnalyzer.getUses(globalScope.root)) {
            String symbolName = use.getSymbolName();
            LexicalScope cscope = ScopeAnalyzer.containingScopeForNode(use.ref.node);
            LexicalScope subScopeOrigin = banned.get(Pair.pair(cscope, symbolName));
            if (subScopeOrigin != null) {
                AncestorChain<?> firstDecl = subScopeOrigin.symbols.getSymbol(symbolName).getDeclarations()
                        .iterator().next();
                mq.addMessage(LinterMessageType.OUT_OF_BLOCK_SCOPE, use.ref.node.getFilePosition(),
                        MessagePart.Factory.valueOf(symbolName), firstDecl.node.getFilePosition());
                continue;
            }
            LexicalScope dscope = cscope.declaringScope(symbolName);
            boolean usedInSameProgramUnitAsDeclared = dscope != null && dscope.inSameProgramUnit(cscope);
            // Keep track of uses of global variables so that we can check
            // @provides and @requires later.
            if (dscope == null) {
                if (!undeclaredGlobals.contains(symbolName)) {
                    mq.addMessage(MessageType.UNDEFINED_SYMBOL, use.ref.node.getFilePosition(),
                            MessagePart.Factory.valueOf(symbolName));
                    undeclaredGlobals.add(symbolName);
                }
            } else {
                if (dscope.isGlobal()) {
                    Map<String, ScopeAnalyzer.Use> m = (!use.isLeftHandSideExpression() ? globalsRead
                            : use.isMemberAccess() ? globalsModified : globalsSet);
                    if (!m.containsKey(symbolName)) {
                        m.put(symbolName, use);
                    }
                }
                // Check liveness
                // Exempt assignments to variables from liveness checks.
                if (usedInSameProgramUnitAsDeclared && !(use.isLeftHandSideExpression() && !use.isMemberAccess())) {
                    if (!(use.ref.parent.node instanceof ExpressionStmt
                            && isForEachLoopKey(use.ref.parent.cast(ExpressionStmt.class)))) {
                        LiveSet liveAtUse = VariableLiveness.livenessFor(use.ref.node);
                        if (liveAtUse != null && !liveAtUse.symbols.contains(Pair.pair(symbolName, dscope))) {
                            mq.addMessage(LinterMessageType.SYMBOL_NOT_LIVE, use.ref.node.getFilePosition(),
                                    MessagePart.Factory.valueOf(symbolName));
                        }
                    }
                }
            }
        }

        // Check @provides and @overrides against the program's free variables.
        checkGlobalsDefined(globalScope, globalScope, Sets.union(provides, overrides), mq);
        for (String symbolName : Sets.difference(globalsSet.keySet(), Sets.union(provides, overrides))) {
            mq.addMessage(MessageType.INVALID_ASSIGNMENT, globalsSet.get(symbolName).ref.node.getFilePosition(),
                    MessagePart.Factory.valueOf(symbolName));
        }
        for (String symbolName : Sets.difference(globalsModified.keySet(), Sets.union(provides, overrides))) {
            ScopeAnalyzer.Use use = globalsModified.get(symbolName);
            mq.addMessage(MessageType.INVALID_ASSIGNMENT, use.ref.node.getFilePosition(),
                    MessagePart.Factory.valueOf(render(use.ref.parent.node)));
        }

        // Check @requires are used
        for (String symbolName : Sets.difference(requires, globalsRead.keySet())) {
            AncestorChain<?> root = globalScope.root;
            mq.addMessage(LinterMessageType.UNUSED_REQUIRE, root.node.getFilePosition().source(),
                    MessagePart.Factory.valueOf(symbolName));
        }
        // TODO(mikesamuel): check locals and formals used

        // Check that @provides are provided
        for (String symbolName : provides) {
            if (!liveAtEnd.symbols.contains(Pair.pair(symbolName, globalScope))) {
                AncestorChain<?> root = globalScope.root;
                mq.addMessage(LinterMessageType.UNUSED_PROVIDE, root.node.getFilePosition().source(),
                        MessagePart.Factory.valueOf(symbolName));
            }
        }
    }

    private static void checkSideEffects(NodeBuckets buckets, MessageQueue mq) {
        // Complain about lack of side-effects
        for (AncestorChain<ExpressionStmt> es : buckets.get(ExpressionStmt.class)) {
            if (shouldBeEvaluatedForValue(es.node.getExpression()) && !isCommaOperatorInForLoop(es)
                    && !isForEachLoopKey(es)) {
                mq.addMessage(MessageType.NO_SIDE_EFFECT, es.node.getFilePosition());
            }
        }
    }

    private static void checkDeadCode(NodeBuckets buckets, MessageQueue mq) {
        // Complain about lack of side-effects
        for (AncestorChain<ExpressionStmt> es : buckets.get(ExpressionStmt.class)) {
            if (VariableLiveness.livenessFor(es.node) == null) {
                // We can't do liveness checks in with blocks, so ignore statements in
                // them.
                boolean isAnalyzable = true;
                for (AncestorChain<?> p = es; p != null; p = p.parent) {
                    if (p.node instanceof WithStmt) {
                        isAnalyzable = false;
                        break;
                    }
                }
                if (isAnalyzable) {
                    mq.addMessage(LinterMessageType.CODE_NOT_REACHABLE, es.node.getFilePosition());
                }
            }
        }
    }

    private static void checkStringsEmbeddable(Block program, NodeBuckets buckets, MessageQueue mq) {
        for (AncestorChain<StringLiteral> lit : buckets.get(StringLiteral.class)) {
            String qval = lit.node.getValue();
            checkEmbeddable(mq, qval, lit.node.getFilePosition());
        }
        for (Token<?> comment : program.getComments()) {
            checkEmbeddable(mq, comment.text, comment.pos);
        }
    }

    private static void checkEmbeddable(MessageQueue mq, String str, FilePosition pos) {
        checkOneEmbeddable(mq, str, pos, "<!");
        checkOneEmbeddable(mq, str, pos, "</script");
        checkOneEmbeddable(mq, str, pos, "]]>");
    }

    private static void checkOneEmbeddable(MessageQueue mq, String str, FilePosition pos, String avoid) {
        str = Strings.lower(str);
        int p = str.indexOf(avoid);
        for (; 0 <= p; p = str.indexOf(avoid, p + 1)) {
            mq.addMessage(LinterMessageType.EMBED_HAZARD, pos.narrowTo(p, avoid.length()),
                    MessagePart.Factory.valueOf(avoid));
        }
    }

    private static String render(ParseTreeNode node) {
        StringBuilder sb = new StringBuilder();
        TokenConsumer tc = node.makeRenderer(sb, null);
        node.render(new RenderContext(tc));
        tc.noMoreTokens();
        return sb.toString();
    }

    /** Encapsulates information about a single input to the linter. */
    public static final class LintJob {
        final InputSource src;
        final Set<String> requires, provides, overrides;
        final Block program;

        LintJob(InputSource src, Set<String> requires, Set<String> provides, Set<String> overrides, Block program) {
            this.src = src;
            this.requires = requires;
            this.provides = provides;
            this.overrides = overrides;
            this.program = program;
        }
    }

    public static final class Environment {
        final Set<String> outers;

        public Environment(Set<String> outers) {
            this.outers = ImmutableSet.copyOf(outers);
        }
    }

    public static final Environment BROWSER_ENVIRONMENT = new Environment(ImmutableSet.of("window", "document",
            "setTimeout", "setInterval", "location", "XMLHttpRequest", "clearInterval", "clearTimeout", "navigator",
            "event", "alert", "confirm", "prompt", "this", "JSON"));

    /**
     * Find identifier lists in documentation comments.
     * Annotations in documentation comments start with a '&#64;' symbol followed
     * by annotationName.
     * The following content ends at the next '@' symbol, and is parsed as an
     * identifier list separated by spaces and/or commas.
     */
    private static final Set<String> parseIdentifierListFromComment(String annotationName, List<Token<?>> comments,
            MessageQueue mq) {
        // TODO(mikesamuel): replace with jsdoc comment parser
        Set<String> idents = Sets.newLinkedHashSet();
        for (Token<?> comment : comments) {
            // Remove line prefixes so they're not interpreted as significant in the
            // middle of an identifier list.
            // And remove trailing content that is not whitespace or commas
            String body = comment.text.replaceAll("\\\\@", "@") // accept "\@" as an annotation prefix
                    .replaceAll("\\*+/$", "").replaceAll("[\r\n]+[ \t]*\\*+[ \t]?", " ");
            String annotPrefix = "@" + annotationName;
            for (int annotStart = -1; (annotStart = body.indexOf(annotPrefix, annotStart + 1)) >= 0;) {
                int annotBodyStart = annotStart + annotPrefix.length();
                int annotBodyEnd = body.indexOf('@', annotBodyStart);
                if (annotBodyEnd < 0) {
                    annotBodyEnd = body.length();
                }
                String annotBody = body.substring(annotBodyStart, annotBodyEnd).trim();
                if ("".equals(annotBody)) {
                    continue;
                }
                // annotBody is the content of an annotation.
                for (String ident : annotBody.split("[\\s,]+")) {
                    if (!ParserBase.isJavascriptIdentifier(ident)) {
                        mq.addMessage(MessageType.INVALID_IDENTIFIER, comment.pos,
                                MessagePart.Factory.valueOf(ident));
                    } else {
                        idents.add(ident);
                    }
                }
            }
        }
        return Collections.unmodifiableSet(idents);
    }

    /**
     * A heuristic that identifies expressions that should not appear in a place
     * where their value cannot be used.  This identifies expressions that don't
     * have a side effect, or that are overly complicated.
     *
     * <p>
     * E.g. the expression {@code [1, 2, 3]} has no side effect and so should not
     * appear where its value would be ignored.
     *
     * <p>
     * The expression {@code +f()} might have a side effect, but the {@code +}
     * operator is redundant, and so the expression should not be ignored.
     *
     * <p>
     * Expressions like function calls and assignments are considered side effects
     * and can reasonably appear where their value is not used.
     *
     * <p>
     * Member access operations {@code a.b} could have a useful side-effect, but
     * are unlikely to be used that way.
     *
     * <p>
     * To convince this method that an operations value is being purposely ignored
     * use the {@code void} operator.
     *
     * @return true for any expression that is likely to be used for its value.
     */
    private static boolean shouldBeEvaluatedForValue(Expression e) {
        // A literal or value constructor
        if (e instanceof Reference || e instanceof Literal || e instanceof ArrayConstructor
                || e instanceof ObjectConstructor || e instanceof FunctionConstructor) {
            return true;
        }
        if (!(e instanceof Operation)) {
            return false;
        }
        Operation op = (Operation) e;
        switch (op.getOperator()) {
        case ASSIGN:
        case DELETE:
        case POST_DECREMENT:
        case POST_INCREMENT:
        case PRE_DECREMENT:
        case PRE_INCREMENT:
        case VOID: // indicates value purposely ignored
            return false;
        case FUNCTION_CALL:
            return false;
        case CONSTRUCTOR:
            return true;
        case LOGICAL_AND:
        case LOGICAL_OR:
            return shouldBeEvaluatedForValue(op.children().get(1));
        case TERNARY:
            return shouldBeEvaluatedForValue(op.children().get(1))
                    && shouldBeEvaluatedForValue(op.children().get(2));
        case COMMA:
            // We do not allow comma, since bad things happen when commas are used.
            // Consider
            //    if (foo)
            //      return bar,
            //    baz();
            // $FALL-THROUGH
        default:
            return op.getOperator().getCategory() != OperatorCategory.ASSIGNMENT;
        }
    }

    private static boolean isCommaOperatorInForLoop(AncestorChain<ExpressionStmt> es) {
        if (es.parent == null || !(es.parent.node instanceof ForLoop)) {
            return false;
        }
        Expression e = es.node.getExpression();

        return isCommaOperationNotEvaluatedForValue(e);
    }

    private static boolean isForEachLoopKey(AncestorChain<ExpressionStmt> es) {
        if (es.parent == null || !(es.parent.node instanceof ForEachLoop)) {
            return false;
        }
        return es.parent.cast(ForEachLoop.class).node.getKeyReceiver() == es.node;
    }

    private static boolean isCommaOperationNotEvaluatedForValue(Expression e) {
        if (!(e instanceof Operation)) {
            return false;
        }
        Operation op = (Operation) e;
        if (op.getOperator() != Operator.COMMA) {
            return false;
        }
        Expression left = op.children().get(0), right = op.children().get(1);
        return !shouldBeEvaluatedForValue(right)
                && (!shouldBeEvaluatedForValue(left) || isCommaOperationNotEvaluatedForValue(left));
    }

    public static void main(String[] args) throws IOException {
        ListIterator<String> argIt = Arrays.asList(args).listIterator();
        List<File> inputs = Lists.newArrayList();
        String outDir = null;
        Set<String> outers = Sets.newLinkedHashSet(BROWSER_ENVIRONMENT.outers);
        Set<String> ignores = Sets.newLinkedHashSet();
        while (argIt.hasNext()) {
            String arg = argIt.next();
            if (!arg.startsWith("-")) {
                argIt.previous();
                break;
            } else if ("--out".equals(arg)) {
                outDir = argIt.next();
            } else if ("--builtin".equals(arg)) {
                outers.add(argIt.next());
            } else if ("--ignore".equals(arg)) {
                ignores.add(argIt.next());
            } else if ("--".equals(arg)) {
                break;
            } else {
                throw new IOException("Unrecognized command line flag " + arg);
            }
        }
        while (argIt.hasNext()) {
            inputs.add(new File(argIt.next()));
        }
        List<File> deps = Lists.newArrayList();
        File out;
        if (outDir == null) {
            out = File.createTempFile(Linter.class.getSimpleName(), ".stamp");
        } else {
            out = new File(outDir, "jslint.txt");
        }
        Environment env = new Environment(outers);
        Map<String, Object> options = Collections.singletonMap("toIgnore", (Object) ignores);
        (new Linter(env)).build(inputs, deps, options, out);
    }

    private static void checkGlobalsDefined(LexicalScope globalScope, LexicalScope scope,
            Set<String> documentedGlobals, MessageQueue mq) {
        for (String symbolName : Sets.difference(Sets.newLinkedHashSet(scope.symbols.symbolNames()),
                documentedGlobals)) {
            for (AncestorChain<?> decl : scope.symbols.getSymbol(symbolName).getDeclarations()) {
                if (decl == globalScope.root) {
                    continue;
                } // a built-in
                if (decl.parent.node instanceof CatchStmt) {
                    continue;
                }
                mq.addMessage(MessageType.UNDOCUMENTED_GLOBAL, decl.node.getFilePosition(),
                        MessagePart.Factory.valueOf(symbolName));
            }
        }
        for (LexicalScope inner : scope.innerScopes) {
            if (!inner.isFunctionScope()) {
                checkGlobalsDefined(globalScope, inner, documentedGlobals, mq);
            }
        }
    }
}