Tuesday, April 9, 2013

JavaScript static analysis and syntax validation with Google Closure compiler

A while back, I found myself needing to have automated syntax checking and static analysis for JavaScript code. I found JSLint to be less-than-ideal, even though it has a Maven plugin. JSLint is fairly hard to tune, and it does a poor job at grokking the syntax and constructs from 3rd-party libraries such as jQuery and YUI. JSLint also tends to be noisy and difficult to tune.

I played around with a few different options and ultimately settled on the Google Closure Compiler. This is a JavaScript minifier/optimizer/compiler which also does (of necessity) a good job at syntax validation and error checking.

I ended up writing an Apache Ant task to invoke Closure on parts of the project source tree, excluding known third-party libraries from analysis. I'm reasonably happy with the results, although I'm sure one day this should be migrated to a Grunt task using the Grunt Closure Compiler plugin.

Without further ado, here is the Ant task definition. Hopefully the in-line comments make the usage pretty clear -- let me know if you find this useful or if you have any questions! Note that this task definition assumes that the Closure compiler JAR file is located in the ant lib directory.

<!--
   <timed-audit-task> is a reusable macro to run a specific audit tool against the source code,
   storing its output under @{audit-output-dir}. The output directory will be deleted and recreated
   prior to running the audit tool. Some basic logging and timing statements are added for clarity
   and profiling.

   To skip the running of a specific tool, the person invoking ant can specifiy -Daudit-skip-<toolname>,
   where <toolname> is the value passed in to the @{audit-task-name} parameter. By convention this should
   be the short name of the tool, for example "findbugs", "checkstyle", or "pmd". Thus, invoking ant with
   -Daudit-skip-findbugs=1 will cause the findbugs audit tool to be skipped. The actual value of the defined
   property is irrelevant.
-->

<macrodef name="timed-audit-task">
   <attribute name="audit-task-name"/>
   <attribute name="audit-output-dir"/>
   <element name="auditTaskBody"/>
   <sequential>
      <if>
         <not><isset property="audit-skip-@{audit-task-name}"/></not>
         <then>
            <echo>Running @{audit-task-name} on ${ant.project.name}</echo>
            <stopwatch name="audit.timer.@{audit-task-name}" action="start"/>
            <delete dir="@{audit-output-dir}"/>
            <mkdir dir="@{audit-output-dir}"/>
            <auditTaskBody/>
            <echo>Finished running @{audit-task-name} on ${ant.project.name}, see @{audit-output-dir}</echo>
            <stopwatch name="audit.timer.@{audit-task-name}" action="total"/>
         </then>
         <else>
            <echo>Skipping @{audit-task-name} because the "audit-skip-@{audit-task-name}" property is set</echo>
         </else>
      </if>
   </sequential>
</macrodef>


<target name="audit-js" description="Runs source code auditing tools for JavaScript">
        <!--
            JavaScript auditing with Google closure
            Warnings flags are defined at http://code.google.com/p/closure-compiler/wiki/Warnings
            The order of parsing JS files is somewhat important here. You should try to pass
            filenames in the rough order they would be parsed by a browser visiting your site or application.

            We use Google's provided "extern" annotated version of jQuery 1.9 to provide additional
            strict error checking. See https://code.google.com/p/closure-compiler/source/browse/contrib/externs/jquery-1.9.js for more information.

            Best place to find documentation on command-line options for the compiler is
            https://code.google.com/p/closure-compiler/source/browse/src/com/google/javascript/jscomp/CommandLineRunner.java
          -->
         <sequential>
            <!-- Exclude known 3rd party scripts from analysis by filename or path -->
            <selector id="audit.js.3rdparty.selector">
               <or>
                  <filename name="scripts/jquery/jquery-*.js"/>
                  <filename name="scripts/yui/**/*.js"/>
               </or>
            </selector>

            <path id="audit.js.3rdparty.path">
               <fileset dir="${source.dir}/html/scripts">
                  <selector refid="audit.js.3rdparty.selector"/>
               </fileset>
            </path>

            <!-- Include our JS source code to be analyzed, excluding 3rd-party stuff defined above -->
            <path id="audit.js.source.path">
               <fileset dir="${source.dir}/html/scripts">
                  <and>
                     <filename name="**/*.js"/>
                     <not>
                        <selector refid="audit.js.3rdparty.selector"/>
                     </not>
                  </and>
               </fileset>
            </path>

            <!-- Pipe compiler output to /dev/null in a platform-sensitive way -->
            <condition property="dev.null" value="NUL" else="/dev/null">
               <os family="windows"/>
            </condition>

            <pathconvert pathsep=" " property="closure.args" refid="audit.js.source.path"/>
            <timed-audit-task audit-task-name="closure-js" audit-output-dir="${closure.dir}">
               <auditTaskBody>
                  <java jar="${ant.home}/lib/closure-compiler.jar" output="${dev.null}" error="${closure.dir}/closure-warnings.txt" fork="true">
                     <arg value="--jscomp_warning=checkRegExp"/>
                     <arg value="--jscomp_off=checkTypes"/>
                     <arg value="--jscomp_off=nonStandardJsDocs"/>
                     <arg value="--jscomp_warning=internetExplorerChecks"/>
                     <arg value="--jscomp_warning=invalidCasts"/>
                     <arg value="--jscomp_off=externsValidation"/>
                     <arg value="--process_jquery_primitives"/>
                     <arg value="--js"/>
                     <arg line="${closure.args}"/>
                  </java>
               </auditTaskBody>
            </timed-audit-task>
         </sequential>
</target>