The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Code::Quality - use static analysis to compute a "code quality" metric for a program

SYNOPSIS

use v5.20;
use Code::Quality;
# code to test (required)
my $code = ...;
# reference code to compare against (optional)
my $reference = ...;

my $warnings =
  analyse_code
    code => $code,
    reference => $reference,
    language => 'C';
if (defined $warnings) {
  my $stars = star_rating_of_warnings $warnings;
  say "Program is rated $stars stars"; # 3 is best, 1 is worst
  my @errors = grep { $_->[0] eq 'error' } @$warnings;
  if (@errors > 0) {
    say 'Found ', scalar @errors, ' errors';
    say "First error:  $errors[0][1]";
  }
} else {
  say 'Failed to analyse code';
}

DESCRIPTION

Code::Quality runs a series of tests on a piece of source code to compute a code quality metric. Each test returns a possibly empty list of warnings, that is potential issues present in the source code. This list of warnings can then be turned into a star rating: 3 stars for good code, 2 stars for acceptable code, and 1 stars for dubious code.

Warnings

A warning is an arrayref [type, message, row, column], where the first two entries are mandatory and the last two can be either both present or both absent. The type is one of qw/error warning info/.

Four-element warnings correspond to ACE code editor annotations. Two-element warnings apply to the entire document, not a specific place in the code.

Tests

A test is a function that takes key-value arguments:

test_something(code => $code, language => $language, [reference => $reference, formatted_code => $formatted])

Here $code is the code to be tested, $language is the programming language, $reference is an optional reference source code to compare $code against, and $formatted_code is the optional result of running $code through a source code formatter.

Each test returns undef if the test failed (for example, if the test cannot be applied to this programming language), and an arrayref of warnings otherwise.

Most tests have several configurable parameters, which come from global variables. The documentation of each test mentions the global variables that affect its operations. local can be used to run a test with special configuration once, without affecting other code:

{
  local $Code::Quality::bla_threshold = 5;
  test_bla code => $code, language => 'C';
}

test_lines

This test counts non-empty lines in both the formatted code and the reference. If no formatted code is available, the original code is used. If the code is significantly longer than the reference, it returns a warning. If the code is much longer, it returns an error. Otherwise it returns an empty arrayref.

The thresholds for raising a warning/error are available in the source code, see global variables @short_code_criteria and @long_code_criteria.

This test fails if no reference is provided, but is language-agnostic

test_clang_tidy

This test runs the clang-tidy static analyser on the code and returns all warnings found.

The clang-tidy checks in use are determined by two global variables, each of which is a list of globs such as modernize-*. The checks in @clang_tidy_warnings produce warnings, while the checks in @clang_tidy_errors produce errors. There is also a hash %clang_tidy_check_options which contains configuration for the checks. Finally, the path to the clang-tidy executable is $clang_tidy_path, which is initialized by looking in the PATH using File::Which. Set this variable to undef to disable this test.

This test does not require a reference, but is limited to languages that clang-tidy understands. This is controlled by the global variable %clang_tidy_extension_of_language, which contains file extensions for the supported languages.

test_lizard

This test runs the lizard.py code complexity analyser on the code, and reports a warning for every function that has high cyclomatic complexity, or that is too long.

The thresholds that determine whether a warning or an error are raised are determined by four global variables, $lizard_warning_loc, $lizard_error_loc, $lizard_warning_ccn, $lizard_error_ccn. Finally, the path to the lizard executable is $lizard_path, which is initialized by looking in the PATH using File::Which. Set this variable to undef to disable this test.

This test does not require a reference, but is limited to languages that lizard understands. This is controlled by the global variable %lizard_extension_of_language, which contains file extensions for the supported languages.

analyse_code

analyse_code runs every test above on the code, producing a combined list of warnings. It fails (returns undef) if all tests fail. The tests run by analyse_code are those in the global variable @all_tests, which is a list of coderefs.

Star rating

star_rating_of_warnings($warnings) is a subroutine that takes the output of a test and computes the star rating as an integer. The rating is undef if the test failed, 1 if the test returned at least one error, 2 if the test returned at least one warning but no errors, and 3 otherwise. So a program gets 3 stars if it only raises informational messages, or no messages at all.

EXPORT

By default only analyse_code and star_rating_of_warnings are exported.

The other tests can be exported on request.

AUTHOR

Marius Gavrilescu, <marius@ieval.ro>

COPYRIGHT AND LICENSE

Copyright (C) 2019 by Wellcode PB SRL

Code::Quality is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Code::Quality is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with Code::Quality. If not, see https://www.gnu.org/licenses/.