Static analysis (Part 1): Code style tools

Trying to explain the issue of code style to a non-programmer usually results in blank stares and raised eyebrows. Arguing about trailing spaces seems like a complete waste of time. And rightly so! Code style rules are important, especially if you are working in a team. But you don’t want to talk about them in every code review. Instead you want to agree on a style once and be done with it.

The actual style is not that important (as long as you agree on spaces instead of tabs, that is 😉 ). Its more important to automate the process of checking (and probably even fixing) the style so you can focus on the actual code.

We’ll be revisiting the SPA tutorial to setup code style tools for the PHP backend as well as the Vue.js frontend. In the process we’ll be looking at CLI tools, IDE integration and also touch on CI pipelines.

Rule sets for the backend

Several PSRs address the topic of code style:

These standards are about relatively basic things like every file should end with a blank line or using 4 spaces to indent the code. Nothing too controversial here.

Other rule sets became popular because they were used by large projects such as WordPress or Symfony. The Symfony Coding Standard is much more sophisticated than the PSRs. It contains highly detailed rules about where to put braces, spaces and blank lines. There are naming conventions for every part of the code and even rules regarding the documentation. It also enforces the usage of Yoda conditions, which we’ll get back to in a minute.

As said before the actual rules are not that important, so we won’t look at them in depth. Just a pick a style you are comfortable with.

Tools for the backend

Today’s IDEs include auto formatting capabilities as well as code style inspections. Both can be configured to your liking. They usually also support a number of external tools for that same purpose which might be surprising at first. But using these tools allows us to easily share the code style rules within the team. We can also use the same tools and rules for CI pipelines.

There are of course lots of code style tools out there, but we’ll only look at PHP-CS-Fixer in detail. PHP-CS-Fixer is part of the Symfony universe. It can be integrated into most of the major IDEs and is super easy to use on the command line. It supports PSR-12 as well as the Symfony standard out of the box. As the name suggests, it doesn’t only detect code style errors, it can also fix them.

Setup PHP-CS-Fixer

PHP-CS-Fixer can be installed in several ways and that is true for all the tools you want to use in a project. Should you

  • install them as a composer (dev) dependency?
  • use as a PHAR file?
  • use a dedicated composer.json for each tool?

Installing code style or code quality tools as a dev dependency seems like the obvious way to go. It allows you to manage all your dependencies from one place. Why would you treat a tool differently then other dependencies?

Well, tools have dependencies of their own. If you add a tool to your project’s dependencies you also add its dependencies. Which means you have to manage them as well. Which also means it’s quite common to have conflicting requirements from your tools. Instead of improving the development process, using several tools can really be a burden if you have to manage lots of conflicting dependencies.

The recommended way to install PHP-CS-Fixer is actually to use a dedicated composer.json. Sadly, they don’t give a reason for this approach. I don’t see any benefit over using a PHAR file. The PHAR file is easily downloaded. Most tools even come with an update feature, so there is no dependency management at all when using a PHAR. PHIVE promises to make this even easier, but trying it is still on my to-do list.

Let’s set up PHP-CS-FIXER using the latest PHAR:

wget https://cs.symfony.com/download/php-cs-fixer-v2.phar -O php-cs-fixer.phar

To fix the code style in our /src directory we simply call:

php php-cs-fixer.phar fix src

This checks the code style using PSR-1 and PSR-2. As I said before, several rule sets are supported out of the box. To check using PSR-12 or the Symfony standard use the --rules option.

php php-cs-fixer.phar fix src/ --rules=@Symfony

One thing I don’t like about the Symfony standard is using Yoda conditions. Luckily it is pretty easy to exclude some rules from an existing rule set by using a config file. This is a small PHP file containing two things:

  • a Finder to specify the files/directories to check/fix
  • the code style rules

.phpcs.dist

<?php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__.'/src')
;

$config = new PhpCsFixer\Config();
return $config->setRules([
        '@Symfony' => true,
        'yoda_style' => false,
    ])
    ->setFinder($finder)
;

The Finder points the tool to the /src directory. The rule config tells it to use the Symfony rule set but remove the need for Yoda conditions from it. The config file is referenced using the --config option:

php php-cs-fixer.phar fix src/ --config=.php_cs.dist

If you want to preview the code changes the --dry-run option comes in handy.

That’s basically it. To make this even more convenient let’s add 2 scripts to our composer.json.

"scripts": {
    "lint": "php php-cs-fixer.phar fix --dry-run -v --config=./.php_cs.dist",
    "fix": "php php-cs-fixer.phar fix -v --config=./.php_cs.dist"
}

The lint script runs the dry run, so it outputs all violations against our code style rules without actually changing the code. This is really nice to learn more about the rules. The fix script fixes all violations. I set both scripts to be verbose so we get a nice output from the tool.

To run the lint script use:

php composer.phar lint

I’m sure you can figure out how to run the fix script from here 😉

PhpStorm integration

PhpStorm supports PHP-CS-Fixer right out of the box. The setup can be very easy because the IDE will recognize the tool in the composer.json and allows you to configure it from there. That’s why the recommend way (by PhpStorm anyway) of using PHP-CS-Fixer is to install it as a Composer dependency. But as we are using the PHAR file we’ll have to do this manually.

This all works fine if you have a working PHP interpreter on your host system. If you want to run the tool through Docker, it is not quite as easy. At first you need to setup a remote interpreter, than you need to configure the PHP-CS-Fixer script to use. In either case you need to setup PHP-CS-Fixer as an inspection afterwards.

In some versions of PhpStorm the Docker approach only works if you are using the absolute path to the PHP-CS-Fixer phar.

PHP-CS-Fixer docker integration

There is bug ticket for that (which I don’t seem to find anymore), so hopefully this is fixed soon.

With this setup the inspection works like any other inspection. There is even the option to fix all violations from within PhpStorm. However, the Docker integration is quite slow. Far from giving instant feedback as the build-in integrations. So setting up a local PHP interpreter might be worth it. Or running the inspection manually. Or relying on the CI pipeline.

CI integration

This will be really high level, as the setup will vary depending on the CI solution you are using. Nevertheless, integrating PHP-CS-Fixer (and other CLI code style tools for that matter) is actually pretty straightforward, especially if you are using some kind of Docker environment. I already covered running the tool from the command line above. As we have the tool as a dependency and the config in the repository, all we need to do is build the project and run the tool via the lint script.

If the script reports an error the CI stage/job should fail. The code style violations can than be fixed locally using the fix script. Committing the fix will result in a passing CI stage/job because we are using the same tool and the same config locally and for the CI process. Yay!

Code style for the frontend

Although often neglected in the past: a code style for the frontend is an important and easy step to improve the code quality. Couple years ago the go-to tool for checking JavaScript code was JSLint. JSLint was not only a tool but also came with a set of code style rules.

A similar approach is taken by ESLint although it is much more flexible. By default it doesn’t enforce any code style, although it comes with it’s own rule set called eslint:recommended. It is up to you if you want to follow this style or come up with your own. ESLint also supports several other code styles out of the box:

There is also support for frameworks like React and Vue.js, which got there own style guides.

As with the PHP-CS-Fixer ESLint can not only detect code style violations but it can also fix many (but not all) of them.

As for which style guide to choose, I’d say: the more restricting the better. Less room for discussions about personal favorites. So I went for StandardJS together with vue-recommend.

Setup ESLint

The setup is pretty easy, as ESLint comes with a nice command line installer. We start by adding ESLint as a dev dependency:

yarn add eslint --dev

You run the installer with:

yarn run eslint --init

It asks a couple of questions about your project and let’s you choose how you want to use the tool.

$ /var/www/html/node_modules/.bin/eslint --init

//Skipping the questions, see the answers below:

✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · vue
✔ Does your project use TypeScript? · No 
✔ Where does your code run? · browser
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · standard
✔ What format do you want your config file to be in? · JavaScript

It then comes up with a list of dependencies you need to install. But when asked if the dependencies should be installed using npm you should decline. We are using yarn for the tutorial and you shouldn’t mix the package managers.

The config that you've selected requires the following dependencies:

eslint-plugin-vue@latest eslint-config-standard@latest eslint@^7.12.1 eslint-plugin-import@^2.22.1 eslint-plugin-no
de@^11.1.0 eslint-plugin-promise@^4.2.1
✔ Would you like to install them now with npm? · No
Successfully created .eslintrc.js file in /var/www/html

Instead just install them using yarn.

yarn add eslint-plugin-vue@latest eslint-config-standard@latest eslint@^7.12.1 eslint-plugin-import@^2.22.1 eslint-plugin-no
de@^11.1.0 eslint-plugin-promise@^4.2.1 --dev

The installer created the config file .eslintrc.js which should look like this:

module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'plugin:vue/essential',
    'standard'
  ],
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module'
  },
  plugins: [
    'vue'
  ],
  rules: {
  }
}

We’ll make 2 adjustments to this config:

  • use the more strict code style from Vue.js: vue/recommended
  • change the indention to 4 instead of 2 spaces to be consistent with the backend
module.exports = {
    env: {
        browser: true,
        es2021: true
    },
    extends: [
        'plugin:vue/recommended',
        'standard'
    ],
    parserOptions: {
        ecmaVersion: 12,
        sourceType: 'module'
    },
    plugins: [
        'vue'
    ],
    rules: {
        indent: ['error', 4],
        'vue/html-indent': ['error', 4]
    }
}

Now we can point the tool at our frontend code and let it do it’s thing:

eslint 'assets/**/*.{js,vue}'

As with the PHP-CS-Fixer before we can make this more convenient by adding this call as a script to the package.json.

"scripts": {
    "lint": "eslint 'assets/**/*.{js,vue}'"
},

We can now run this like any other script:

yarn run lint

Which results in lot’s of warnings, most of them fixable by ESLint:

$ eslint 'assets/**/*.{js,vue}'

/var/www/html/assets/vue/components/MovieDetails.vue
   5:40  warning  Expected 1 line break after opening tag (`<h5>`), but no line breaks found    vue/singleline-html
-element-content-newline
   5:51  warning  Expected 1 line break before closing tag (`</h5>`), but no line breaks found  vue/singleline-html
-element-content-newline
   6:38  warning  Expected 1 line break after opening tag (`<p>`), but no line breaks found     vue/singleline-html
-element-content-newline
   6:55  warning  Expected 1 line break before closing tag (`</p>`), but no line breaks found   vue/singleline-html
-element-content-newline
  15:13  warning  Prop "title" should define at least its type                                  vue/require-prop-ty
pes
  15:22  warning  Prop "description" should define at least its type                            vue/require-prop-ty
pes

/var/www/html/assets/vue/views/AddMovie.vue
   4:27   warning  'class' should be on a new line                         vue/max-attributes-per-line
   4:55   warning  'role' should be on a new line                          vue/max-attributes-per-line
  11:13   warning  Disallow self-closing on HTML void elements (<input/>)  vue/html-self-closing
  11:32   warning  'class' should be on a new line                         vue/max-attributes-per-line
  11:53   warning  Attribute "id" should go before "class"                 vue/attributes-order
  11:53   warning  'id' should be on a new line                            vue/max-attributes-per-line
  11:64   warning  'v-model' should be on a new line                       vue/max-attributes-per-line
  11:64   warning  Attribute "v-model" should go before "class"            vue/attributes-order
  11:80   warning  '@input' should be on a new line                        vue/max-attributes-per-line
  11:102  warning  Expected a space before '/>', but not found             vue/html-closing-bracket-spacing
  17:13   warning  Disallow self-closing on HTML void elements (<input/>)  vue/html-self-closing
  17:32   warning  'class' should be on a new line                         vue/max-attributes-per-line
  17:53   warning  Attribute "id" should go before "class"                 vue/attributes-order
  17:53   warning  'id' should be on a new line                            vue/max-attributes-per-line
  17:70   warning  Attribute "v-model" should go before "class"            vue/attributes-order
  17:70   warning  'v-model' should be on a new line                       vue/max-attributes-per-line
  17:92   warning  '@input' should be on a new line                        vue/max-attributes-per-line
  17:114  warning  Expected a space before '/>', but not found             vue/html-closing-bracket-spacing
  19:41   warning  '@click' should be on a new line                        vue/max-attributes-per-line

/var/www/html/assets/vue/views/MovieList.vue
  9:17  warning  Attribute ":key" should go before ":description"  vue/attributes-order

✖ 26 problems (0 errors, 26 warnings)
  0 errors and 17 warnings potentially fixable with the `--fix` option.

You can use the --fix option for that.

PHPStorm integration

ESLint is supported by PHPStorm, but it is disabled by default. Once activated it can automatically detect the right ESLint package as well as the rules config. You can of course also set these things up manually. If you have a local Node interpreter you’re pretty much done. You get the inspection results as you type.

Sadly there is currently no support for using ESLint through docker. The feature was requested since 2017 but to this day it wasn’t implemented. There are however some comments by a JetBrains employee which might suggest that the docker integration is indeed coming.

CI integration

This is pretty similar to the PHP-CS-Fixer integration. We just need to build the frontend and then run the lint script. If there are any errors than the stage/job should fail.

Conclusion

To wrap this up: picking a code style and setting up automatic checks are easy steps to increase the code quality of any project. Even better, they help to keep code reviews focused. Use the same tools and rules for the local environment and the CI process. Don’t neglect the frontend.

IDE integration can sometimes be a little bit troublesome. One way to address this is to set the same code style rules on the internal tools that you have setup for the external tools. The internal tools are usually better integrated and can also be much faster. If a CI job fails because of a code style violation, fix them manually. This way you learn the rules in no time.

You can find the current version of the code in the repository.

The series continues with Part 2: Code quality tools.