This post is the next step from my PHP Code Static Analysis post. Once I had a way to run static analysis on my project with just one command, I wanted to make this an automatic check every time something was committed.

This would have been a super simple task except that there was one feature that didn’t come out of the box from my build file. I wanted the build to fail when PHPDoc issues a warning so code is not committed without their respective doc blocks.

Git hooks

Git hooks are really easy to create. You just need to go to .git/hooks/ in your repository and you will find a list of the available hooks followed by the .sample extension. To activate the hook you just need to remove the .sample extension and give the file execution permissions. The content of the hook can be any script that can be executed from a terminal.

Pre commit hook

Because what I want to do is stop a commit from going through when there are any static analysis warnings I wanted to use the pre-commit hook. This hooks will be executed every time a commit is issued, before it is saved in the repository history. If the script exits with an exit code of 0 the commit will go through, but if it exits with any other exit code it will stop.

PHP Static Analysis hook

My static analysis hook will run just one command:

1
ant build

And it will execute this build file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<project basedir="." name="myapp">
    <target name="build" depends="phpcs,phpmd,phpcpd,phpdoc"/>

    <target name="phpcs" description="Run PHP Code Sniffer">
        <exec executable="phpcs" failonerror="true">
            <arg value="--standard=Zend"/>
            <arg value="application"/>
            <arg value="public/css"/>
        </exec>
    </target>

    <target name="phpmd" description="Run PHP Mess Detector">
        <exec executable="phpmd" failonerror="true">
            <arg value="application"/>
            <arg value="text"/>
            <arg value="codesize,unusedcode,naming"/>
        </exec>
    </target>

    <target name="phpcpd" description="Run PHP copy-paste detector">
        <exec executable="phpcpd" failonerror="true">
            <arg value="application"/>
        </exec>
    </target>

    <target name="phpdoc" description="Run PHPDoc">
        <exec executable="phpdoc" failonerror="true">
            <arg value="-d"/>
            <arg value="application"/>
            <arg value="-t"/>
            <arg value="docs"/>
            <arg value="-i"/>
            <arg value="application/views/,application/layouts/"/>
            <arg value="--force"/>
            <arg value="--template"/>
            <arg value="checkstyle"/>
        </exec>
    </target>
</project>

I added failonerror=”true” to all my exec commands so they send an exit code different to 0 every time a warning is thrown. As I mentioned earlier, this would have made the script extremely simple if it wasn’t for one extra requirement I had, I wanted the build to fail when there where PHPDoc warnings.

By default PHPDoc doesn’t fail unless there is a compilation error on the files, so even when there are warnings the exit code would be 0.

To overcome this issue I decided to use a hack to find out if there were any warnings issued by PHPDoc and make the hook exit with a code different than 0 if there were any. My hack consisted of reading the contents of the checkstyle.xml file generated by PHPDoc. This file contains all the warnings found by PHPDoc in an XML format.

Because I wanted to make my script as simple as possible I opted to use a very cheap approach (which I am not sure if will work in all scenarios): When there are no errors checkstyle.xml will only contain two lines:

1
2
<?xml version="1.0"?>
<checkstyle version="1.3.0"/>

So I just consider PHPDoc clean if checkstyle.xml has only two lines.

I used sh for my script. And at the end it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#! /bin/sh

# This file should be placed under .git/hooks/ to run Static Analysis every time
# a commit is made.

# Run the build
ant build
exitCode=$?

# PHPDoc gives an exit code of 0 even when there are errors so we will check the
# checkstyle file to see if there are errors
if [ 0 = $exitCode ]
then
    # Checkstyle file has only two lines when there are no errors
    numLines=$(wc -l docs/checkstyle.xml | awk -F " " '{print $1}')
    if [ 2 -lt $numLines ]
    then
        exitCode=1
    fi
fi

# If exit code is not 0 then there was a failure
if [ 0 -ne $exitCode ]
then
    echo "\033[01;31mStatic Analysis failed\033[00m"
    exit 1
fi

echo "\033[01;32mStatic Analysis completed successfully\033[00m"
exit 0

I am not a shell scripting expert so I might have created a very inefficient script, but for now I know it does the job. I added some comments so it is clear what I am doing on each section but I will explain a little of the syntax for all people not familiar with shell scripting reading my article.

1
2
3
# Run the build
ant build
exitCode=$?

For those who don’t know $? grabs the exit code of the last executed command. I am assigning it to a variable for later use.

1
numLines=$(wc -l docs/checkstyle.xml | awk -F " " '{print $1}')

When you want to assign the output of a command to a variable you can wrap the command on $(). In this line I am first executing wc (word count) with the -l modifier, which makes it count the number of lines. That would have been enough, but the output of that command is something like this:

1
2 docs/checkstyle.xml

And I needed just the number of lines (2). So I send the output to awk, which is a command that helps you deal with column based files. What I am telling it to do is to use a blank space (” “) as the column separator and to print only the first column ({print $1}). This effectively returns just the number I am looking for.

The rest of the script is hopefully easy to understand.

[ automation  bash  git  php  programming  testing  ]
Testing in Rust
Monitoring Kubernetes Resources with Fabric8 Informers
Introduction to CircleCI
Using testify for Golang tests
Unit testing Golang code