Revision: 3876
Author: [email protected]
Date: Mon Nov 30 20:14:27 2009
Log: updates to the ancillary linter
http://code.google.com/p/google-caja/source/detail?r=3876

Modified:
 /trunk/src/com/google/caja/ancillary/linter/LexicalScope.java
 /trunk/src/com/google/caja/ancillary/linter/Linter.java
 /trunk/src/com/google/caja/ancillary/linter/VariableLiveness.java
 /trunk/tests/com/google/caja/ancillary/linter/LinterTest.java
 /trunk/tests/com/google/caja/ancillary/linter/VariableLivenessTest.java

=======================================
--- /trunk/src/com/google/caja/ancillary/linter/LexicalScope.java Mon Oct 19 18:04:03 2009 +++ /trunk/src/com/google/caja/ancillary/linter/LexicalScope.java Mon Nov 30 20:14:27 2009
@@ -18,6 +18,9 @@
 import com.google.caja.parser.js.CatchStmt;
 import com.google.caja.parser.js.FunctionConstructor;
 import com.google.caja.parser.js.WithStmt;
+import com.google.caja.util.Lists;
+
+import java.util.List;

 /**
* A set of adjacent AST nodes that share a common ancestor which is in the set.
@@ -54,11 +57,13 @@
   final AncestorChain<?> root;
   final LexicalScope parent;
   final SymbolTable symbols;
+  final List<LexicalScope> innerScopes = Lists.newArrayList();

   LexicalScope(AncestorChain<?> root, LexicalScope parent) {
     this.root = root;
     this.parent = parent;
     this.symbols = new SymbolTable();
+    if (parent != null) { parent.innerScopes.add(this); }
   }

   LexicalScope declaringScope(String symbolName) {
=======================================
--- /trunk/src/com/google/caja/ancillary/linter/Linter.java Wed Nov 18 16:37:58 2009 +++ /trunk/src/com/google/caja/ancillary/linter/Linter.java Mon Nov 30 20:14:27 2009
@@ -18,7 +18,6 @@
 import com.google.caja.lexer.InputSource;
 import com.google.caja.lexer.JsLexer;
 import com.google.caja.lexer.JsTokenQueue;
-import com.google.caja.lexer.JsTokenType;
 import com.google.caja.lexer.ParseException;
 import com.google.caja.lexer.Token;
 import com.google.caja.lexer.TokenConsumer;
@@ -27,6 +26,7 @@
 import com.google.caja.parser.ParserBase;
 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;
@@ -39,6 +39,7 @@
 import com.google.caja.parser.js.Loop;
 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;
@@ -85,7 +86,7 @@
     Map<InputSource, CharSequence> contentMap = Maps.newLinkedHashMap();
     MessageQueue mq = new SimpleMessageQueue();
     List<LintJob> lintJobs = parseInputs(inputs, contentMap, mc, mq);
-    lint(lintJobs, mq);
+    lint(lintJobs, new Environment(Sets.<String>newHashSet()), mq);
     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
@@ -114,15 +115,8 @@
       JsTokenQueue tq = new JsTokenQueue(new JsLexer(cp), src);
       try {
         if (tq.isEmpty()) { continue; }
-        List<Token<JsTokenType>> tokens = tq.filteredTokens();
         Parser p = new Parser(tq, mq);
-        compUnits.add(
-            new LintJob(
-                src,
-                parseIdentifierListFromComment("requires", tokens, mq),
-                parseIdentifierListFromComment("provides", tokens, mq),
-                parseIdentifierListFromComment("overrides", tokens, mq),
-                p.parse()));
+        compUnits.add(makeLintJob(p.parse(), mq));
       } catch (ParseException ex) {
         ex.toMessageQueue(mq);
       }
@@ -130,10 +124,21 @@
     return compUnits;
   }

-  // Visible for testing
-  static void lint(List<LintJob> jobs, MessageQueue mq) {
+  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),
+      lint(AncestorChain.instance(job.program), env,
            // Anything defined by this file can be read by this file.
            job.provides, job.requires, job.overrides, mq);
     }
@@ -157,7 +162,7 @@
    * @param mq receives messages about violations of canRead and canSet.
    */
   private static void lint(
-      AncestorChain<?> ac,
+      AncestorChain<?> ac, final Environment env,
       Set<String> provides, final Set<String> requires,
       final Set<String> overrides,
       MessageQueue mq) {
@@ -186,7 +191,8 @@
             containing.symbols.declare(fc.getIdentifierName(), scope.root);
           }
         } else if (scope.isGlobal()) {
-          for (String symbolName : Sets.union(requires, overrides)) {
+          for (String symbolName : Sets.union(
+                   env.outers, Sets.union(requires, overrides))) {
             if (scope.symbols.getSymbol(symbolName) == null) {
               scope.symbols.declare(symbolName, scope.root);
             }
@@ -241,6 +247,8 @@
           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()
@@ -248,8 +256,7 @@
                    : MessageLevel.ERROR),
                   declarations.iterator().next().node.getFilePosition(),
                   MessagePart.Factory.valueOf(symbolName),
-                  masked.getDeclarations().iterator()
-                      .next().node.getFilePosition());
+                  firstMasked.node.getFilePosition());
             }
             if (p.isFunctionScope()) { break; }
           }
@@ -331,12 +338,13 @@
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),
-            (subScopeOrigin.symbols.getSymbol(symbolName).getDeclarations()
-             .iterator().next().node.getFilePosition()));
+            firstDecl.node.getFilePosition());
         continue;
       }
       LexicalScope dscope = cscope.declaringScope(symbolName);
@@ -380,17 +388,8 @@
     }

     // Check @provides and @overrides against the program's free variables.
-    for (String symbolName : Sets.difference(
-             globalScope.symbols.symbolNames(),
-             Sets.union(provides, overrides))) {
-      for (AncestorChain<?> decl
-           : globalScope.symbols.getSymbol(symbolName).getDeclarations()) {
-        if (decl == globalScope.root) { continue; }  // a built-in
-        mq.addMessage(
-            MessageType.UNDOCUMENTED_GLOBAL, decl.node.getFilePosition(),
-            MessagePart.Factory.valueOf(symbolName));
-      }
-    }
+    checkGlobalsDefined(
+        globalScope, globalScope, Sets.union(provides, overrides), mq);
     for (String symbolName : Sets.difference(
              globalsSet.keySet(), Sets.union(provides, overrides))) {
       mq.addMessage(
@@ -408,9 +407,10 @@

     // Check @requires are used
for (String symbolName : Sets.difference(requires, globalsRead.keySet())) {
+      AncestorChain<?> root = globalScope.root;
       mq.addMessage(
           LinterMessageType.UNUSED_REQUIRE,
-          globalScope.root.node.getFilePosition().source(),
+          root.node.getFilePosition().source(),
           MessagePart.Factory.valueOf(symbolName));
     }
     // TODO(mikesamuel): check locals and formals used
@@ -418,9 +418,10 @@
     // 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,
-            globalScope.root.node.getFilePosition().source(),
+            root.node.getFilePosition().source(),
             MessagePart.Factory.valueOf(symbolName));
       }
     }
@@ -466,7 +467,7 @@
   }

   /** Encapsulates information about a single input to the linter. */
-  static final class LintJob {  // Visible for testing
+  public static final class LintJob {
     final InputSource src;
     final Set<String> requires, provides, overrides;
     final Block program;
@@ -481,6 +482,20 @@
     }
   }

+  public static final class Environment {
+    final Set<String> outers;
+
+    public Environment(Set<String> outers) {
+      this.outers = Sets.immutableSet(outers);
+    }
+  }
+
+  public static final Environment BROWSER_ENVIRONMENT = new Environment(
+      Sets.newLinkedHashSet(
+          "window", "document", "setTimeout", "setInterval", "location",
+          "XMLHttpRequest", "clearInterval", "clearTimeout", "navigator",
+          "event", "alert", "confirm", "prompt", "this"));
+
   /**
    * Find identifier lists in documentation comments.
* Annotations in documentation comments start with a '&#64;' symbol followed
@@ -489,11 +504,11 @@
    * identifier list separated by spaces and/or commas.
    */
   private static final Set<String> parseIdentifierListFromComment(
-      String annotationName, List<Token<JsTokenType>> comments,
+      String annotationName, List<Token<?>> comments,
       MessageQueue mq) {
     // TODO(mikesamuel): replace with jsdoc comment parser
     Set<String> idents = Sets.newLinkedHashSet();
-    for (Token<JsTokenType> comment : comments) {
+    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
@@ -567,7 +582,20 @@
         Expression left = op.children().get(0);
         return left instanceof Operation
             && Operator.CONSTRUCTOR == ((Operation) left).getOperator();
-      default: return op.getOperator().getAssignmentDelegate() == null;
+      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;
     }
   }

@@ -605,4 +633,26 @@
     File out = File.createTempFile(Linter.class.getSimpleName(), ".stamp");
     (new Linter()).build(inputs, deps, out);
   }
-}
+
+  private static void checkGlobalsDefined(
+      LexicalScope globalScope, LexicalScope scope,
+      Set<String> documentedGlobals, MessageQueue mq) {
+    for (String symbolName : Sets.difference(
+            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);
+      }
+    }
+  }
+}
=======================================
--- /trunk/src/com/google/caja/ancillary/linter/VariableLiveness.java Mon Nov 16 17:29:55 2009 +++ /trunk/src/com/google/caja/ancillary/linter/VariableLiveness.java Mon Nov 30 20:14:27 2009
@@ -24,6 +24,7 @@
 import com.google.caja.parser.js.ContinueStmt;
 import com.google.caja.parser.js.Declaration;
 import com.google.caja.parser.js.DefaultCaseStmt;
+import com.google.caja.parser.js.DirectivePrologue;
 import com.google.caja.parser.js.DoWhileLoop;
 import com.google.caja.parser.js.Expression;
 import com.google.caja.parser.js.ExpressionStmt;
@@ -47,7 +48,6 @@
 import com.google.caja.parser.js.TryStmt;
 import com.google.caja.parser.js.WhileLoop;
 import com.google.caja.parser.js.WithStmt;
-import com.google.caja.parser.js.DirectivePrologue;
 import com.google.caja.util.SyntheticAttributeKey;

 import java.util.List;
@@ -209,8 +209,7 @@
     } else if (s instanceof LabeledStmtWrapper) {
       return processLabeledStmtWrapper((LabeledStmtWrapper) s, onEntry);
     } else if (s instanceof DirectivePrologue) {
-      // TODO: New node type, not yet supported by linter
-      return processDirectivePrologue((DirectivePrologue) s, onEntry);
+      return processDirectivePrologue(onEntry);
     } else {
       throw new RuntimeException(s.getClass().getName());
     }
@@ -675,10 +674,8 @@
     return last;
   }

-  private static LiveCalc processDirectivePrologue(
-      DirectivePrologue d, LiveSet onEntry) {
-    // TODO: New node type, not yet supported by linter
-    return processNoop(onEntry);
+  private static LiveCalc processDirectivePrologue(LiveSet onEntry) {
+    return new LiveCalc(onEntry, ExitModes.COMPLETES);
   }

   /**
=======================================
--- /trunk/tests/com/google/caja/ancillary/linter/LinterTest.java Wed Nov 18 16:37:58 2009 +++ /trunk/tests/com/google/caja/ancillary/linter/LinterTest.java Mon Nov 30 20:14:27 2009
@@ -58,6 +58,9 @@
              .make()),
         "ERROR: testProvides: @provides y not provided"
         );
+    runLinterTest(
+        jobs(new LintJobMaker(js(fromString("for (var i;;) {}"))).make()),
+        "LINT: testProvides:1+6 - 11: Undocumented global i");
   }

   public final void testMultiplyProvidedSymbols() throws Exception {
@@ -288,7 +291,8 @@
         );
     runLinterTest(
         jobs(new LintJobMaker(js(fromString("for (var k in o);"))).make()),
-        "ERROR: testLoops:1+15 - 16: Symbol o has not been defined");
+        "ERROR: testLoops:1+15 - 16: Symbol o has not been defined",
+        "LINT: testLoops:1+6 - 11: Undocumented global k");
   }

   public final void testIgnoredValue() throws Exception {
@@ -298,7 +302,7 @@
             + "var c; f;  \n"  // Line 1
             + "+n;  \n"  // line 2
             + "a.b;  \n"  // line 3
-            + "f && g();  \n"  // line 4
+ + "f && g(); f && g; f() && g; \n" // line 4: 1st OK, others not
             + "a = b, c = d;  \n"  // line 5
             + "a = b;  \n"  // OK
             + "m += n;  \n"  // OK
@@ -306,7 +310,8 @@
             + "new Array;  \n"  // line 9
             + "new Array();  \n"  // line 10
             + "for (a = b, c = d; !a; ++a, --m, ++c) f;  \n"  // line 11
-            + "while (1) { 1; }"  // line 12.  First allowed, second not
+            + "++c;  \n"  // OK
+            + "while (1) { 1; }"  // line 13.  First allowed, second not
             //          1         2         3         4
             // 1234567890123456789012345678901234567890
             )))
@@ -317,12 +322,13 @@
         "WARNING: testIgnoredValue:1+8 - 9: Operation has no effect",
         "WARNING: testIgnoredValue:2+1 - 3: Operation has no effect",
         "WARNING: testIgnoredValue:3+1 - 4: Operation has no effect",
-        "WARNING: testIgnoredValue:4+1 - 9: Operation has no effect",
+        "WARNING: testIgnoredValue:4+12 - 18: Operation has no effect",
+        "WARNING: testIgnoredValue:4+21 - 29: Operation has no effect",
         "WARNING: testIgnoredValue:5+1 - 13: Operation has no effect",
         "WARNING: testIgnoredValue:9+1 - 10: Operation has no effect",
         "WARNING: testIgnoredValue:10+1 - 12: Operation has no effect",
         "WARNING: testIgnoredValue:11+39 - 40: Operation has no effect",
-        "WARNING: testIgnoredValue:12+13 - 14: Operation has no effect");
+        "WARNING: testIgnoredValue:13+13 - 14: Operation has no effect");
   }

   public final void testDeadCode() throws Exception {
@@ -509,7 +515,7 @@

private void runLinterTest(List<Linter.LintJob> inputs, String... messages) {
     MessageQueue mq = new SimpleMessageQueue();
-    Linter.lint(inputs, mq);
+ Linter.lint(inputs, new Linter.Environment(Sets.<String>newHashSet()), mq);
     List<String> actualMessageStrs = Lists.newArrayList();
     for (Message msg : mq.getMessages()) {
       actualMessageStrs.add(
=======================================
--- /trunk/tests/com/google/caja/ancillary/linter/VariableLivenessTest.java Mon Oct 19 18:04:03 2009 +++ /trunk/tests/com/google/caja/ancillary/linter/VariableLivenessTest.java Mon Nov 30 20:14:27 2009
@@ -1468,6 +1468,14 @@
         "testLiveBreaks:27", formatShort(stmts.next().getFilePosition()));
     assertFalse(stmts.hasNext());
   }
+
+  public final void testDirectives() throws Exception {
+    assertLiveness(
+        js(fromString("\"use strict\";")),
+        "Block",
+        "  DirectivePrologue",
+        "    Directive : use strict");
+  }

   private static ExitModes setup(ParseTreeNode js) {
     ScopeAnalyzer sa = new ScopeAnalyzer();

Reply via email to