Java tutorial
// 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 '@' 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); } } } }