PVS-Studio and Continuous Integration: TeamCity. Analysis of the Open RollerCoaster Tycoon 2 project


    One of the most relevant scenarios for using the PVS-Studio analyzer is its integration into CI systems. Even though a project analysis by PVS-Studio can already be embedded with just a few commands into almost any continuous integration system, we continue to make this process even more convenient. PVS-Studio now supports converting the analyzer output to the TeamCity format-TeamCity Inspections Type. Let's see how it works.

    About the software used


    PVS-Studio is a static analyzer of C, C++, C#, and Java code designed to facilitate the task of finding and correcting various types of errors. The analyzer can be used on Windows, Linux, and macOS. In this article, we will actively use not only the analyzer itself, but also some utilities from its distribution.

    CLMonitor is a monitoring server that performs monitoring of compiler runs. It has to be run immediately before building a project. In monitoring mode, the server will intercept runs of all supported compilers. It is worth noting that this utility can only be used for analyzing C/C++ projects.

    PlogConverter is a utility for converting the analyzer report into different formats.

    About the checked project


    Let's try this feature on a practical example by analyzing the OpenRCT2 project.

    OpenRCT2 is an open implementation of the RollerCoaster Tycoon 2 (RCT2) game, expanding it with new features and fixed bugs. The gameplay revolves around the construction and maintenance of an amusement park that houses rides, stores, and facilities. The player has to try to make profit and maintain the good reputation of the park, while keeping the guests happy. OpenRCT2 allows you to play both by following the script and in the sandbox. Scenarios require a player to complete a specific task in a set time, while the sandbox allows a player to build a more flexible park without any restrictions or finances.

    Configuration


    In order to save time, I will probably skip the installation process and start from the point when the TeamCity server is running on my computer. We need to go to: localhost:{the port specified during installation}(in my case, localhost:9090) and enter the authorization data. After entering we will get:

    image3.png

    Click on Create Project. Next, select Manually and fill in the fields.

    image5.png

    After clicking Create, we see the window with settings.

    image7.png

    Click Create build configuration.

    image9.png

    Fill in the fields and click Create. We see the window suggesting to select a version control system. Since the sources are already located locally, click Skip.

    image11.png

    Finally, we go to the project settings.

    image13.png

    We'll add the build steps. To do this, click: Build steps -> Add build step.

    image15.png

    Here we choose:

    • Runner type -> Command Line
    • Run -> Custom Script

    Since we will perform analysis during the project compilation, the build and analysis must be one step, therefore we will fill in the Custom Script field:

    image17.png

    We will focus on individual steps later. It is important that loading the analyzer, building the project, analyzing it, the report output, and formatting it should take only eleven lines of code.

    The last thing we need to do is to set environment variables, which, in my case, outline some ways to improve their readability. To do this, go to: Parameters -> Add new parameter and add three variables:

    image19.png

    Just click on Run in the upper-right corner. While the project is being built and analyzed, let me will tell you about the script.

    The script itself


    First, we need to download the latest PVS-Studio distribution. To do this, we use the Chocolatey package manager. For those who want to learn more about this, there is a special article:

    choco install pvs-studio -y

    Next, run the CLMonitor project build monitoring utility.

    %CLmon% monitor –-attach

    Then we will build the project. The MSB environment variable represents the path to the MSBuild version that I need to build.

    %MSB% %ProjPath% /t:clean
    %MSB% %ProjPath% /t:rebuild /p:configuration=release
    %MSB% %ProjPath% /t:g2
    %MSB% %ProjPath% /t:PublishPortable

    Enter the username and license key for PVS-Studio:

    %PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%

    After the build is complete, we will run CLMonitor again to generate preprocessed files and perform static analysis:

    %CLmon% analyze -l "c:\ptest.plog"

    After that, we will use another utility from our distribution. PlogConverter converts a report from standard to a TeamCity-specific format. This allows us to view it directly in the build window.

    %PlogConverter% "c:\ptest.plog" --renderTypes=TeamCity -o "C:\temp"

    The last action is to output the formatted report to stdout, where it will be picked up by the TeamCity parser.

    type "C:\temp\ptest.plog_TeamCity.txt"

    Full script code:

    choco install pvs-studio -y
    %CLmon% monitor --attach
    set platform=x64
    %MSB% %ProjPath% /t:clean
    %MSB% %ProjPath% /t:rebuild /p:configuration=release
    %MSB% %ProjPath% /t:g2
    %MSB% %ProjPath% /t:PublishPortable
    %PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%
    %CLmon% analyze -l "c:\ptest.plog"
    %PlogConverter% "c:\ptest.plog" --renderTypes=TeamCity -o "C:\temp"
    type "C:\temp\ptest.plog_TeamCity.txt"

    In the meantime, the build and analysis of the project has been completed successfully, so we can go to the Projects tab and make sure of it.

    image21.png

    Now click on Inspections Total to view the analyzer report:

    image23.png

    Warnings are grouped by diagnostic rule numbers. To navigate along the code, click on the line number with the warning. Clicking on the question mark in the upper-right corner will open a new tab with documentation. You can also navigate along the code by clicking on the line number with the analyzer warning. Navigation from a remote computer is possible when using the SourceTreeRoot marker. Those who are interested in this mode of the analyzer operation are welcome to read the related documentation section.

    Viewing the analysis results


    After we are done with the deployment and configuration of the build, I suggest taking a look at some interesting warnings found in the reviewed project.

    Warning N1

    V773 [CWE-401] The exception was thrown without releasing the 'result' pointer. A memory leak is possible. libopenrct2 ObjectFactory.cpp 443

    Object* CreateObjectFromJson(....)
    {
      Object* result = nullptr;
      ....
      result = CreateObject(entry);
      ....
      if (readContext.WasError())
      {
        throw std::runtime_error("Object has errors");
      }
      ....
    }
    
    Object* CreateObject(const rct_object_entry& entry)
    {
      Object* result;
      switch (entry.GetType())
      {
        case OBJECT_TYPE_RIDE:
          result = new RideObject(entry);
          break;
        case OBJECT_TYPE_SMALL_SCENERY:
          result = new SmallSceneryObject(entry);
          break;
        case OBJECT_TYPE_LARGE_SCENERY:
          result = new LargeSceneryObject(entry);
          break;
        ....
        default:
          throw std::runtime_error("Invalid object type");
      }
      return result;
    }

    The analyzer noticed the error that after dynamic memory allocation in CreateObject, when an exception occurs, the memory is not cleared, and consequently, a memory leak occurs.

    Warning N2

    V501 There are identical sub-expressions '(1ULL << WIDX_MONTH_BOX)' to the left and to the right of the '|' operator. libopenrct2ui Cheats.cpp 487

    static uint64_t window_cheats_page_enabled_widgets[] = 
    {
      MAIN_CHEAT_ENABLED_WIDGETS |
      (1ULL << WIDX_NO_MONEY) |
      (1ULL << WIDX_ADD_SET_MONEY_GROUP) |
      (1ULL << WIDX_MONEY_SPINNER) |
      (1ULL << WIDX_MONEY_SPINNER_INCREMENT) |
      (1ULL << WIDX_MONEY_SPINNER_DECREMENT) |
      (1ULL << WIDX_ADD_MONEY) |
      (1ULL << WIDX_SET_MONEY) |
      (1ULL << WIDX_CLEAR_LOAN) |
      (1ULL << WIDX_DATE_SET) |
      (1ULL << WIDX_MONTH_BOX) |  // <=
      (1ULL << WIDX_MONTH_UP) |
      (1ULL << WIDX_MONTH_DOWN) |
      (1ULL << WIDX_YEAR_BOX) |
      (1ULL << WIDX_YEAR_UP) |
      (1ULL << WIDX_YEAR_DOWN) |
      (1ULL << WIDX_DAY_BOX) |
      (1ULL << WIDX_DAY_UP) |
      (1ULL << WIDX_DAY_DOWN) |
      (1ULL << WIDX_MONTH_BOX) |  // <=
      (1ULL << WIDX_DATE_GROUP) |
      (1ULL << WIDX_DATE_RESET),
      ....
    };

    Few, but a static code analyzer would be able to pass this test for attention. It is diligence that this example of copy paste checks.

    Warnings N3

    V703 It is odd that the 'flags' field in derived class 'RCT12BannerElement' overwrites field in base class 'RCT12TileElementBase'. Check lines: RCT12.h:570, RCT12.h:259. libopenrct2 RCT12.h 570

    struct RCT12SpriteBase
    {
      ....
      uint8_t flags;
      ....
    };
    struct rct1_peep : RCT12SpriteBase
    {
      ....
      uint8_t flags;
      ....
    };

    Of course, using the same name variable both in the base and derived classes is not always an error. However, inheritance technology itself assumes that all fields of the parent class are present in the child class. By declaring a field with the same name in the derived class, we create confusion.

    Warning N4

    V793 It is odd that the result of the 'imageDirection / 8' statement is a part of the condition. Perhaps, this statement should have been compared with something else. libopenrct2 ObservationTower.cpp 38

    void vehicle_visual_observation_tower(...., int32_t imageDirection, ....)
    {
      if ((imageDirection / 8) && (imageDirection / 8) != 3)
      {
        ....
      }
      ....
    }

    Let's look at it in more detail. The imageDirection / 8 expression will be false if imageDirection is in the range from -7 to 7. Second part: (imageDirection / 8) != 3 checks imageDirection for being outside the range: from -31 to -24 and from 24 to 31, respectively. It seems rather strange to check numbers for falling into a certain range in this way, and even if there is no error in this code fragment, I would recommend rewriting these conditions to more explicit ones. This would significantly simplify the lives of people who will read and maintain this code afterwards.

    Warning N5

    V587 An odd sequence of assignments of this kind: A = B; B = A;. Check lines: 1115, 1118. libopenrct2ui MouseInput.cpp 1118

    void process_mouse_over(....)
    {
      ....
      switch (window->widgets[widgetId].type)
      {
        case WWT_VIEWPORT:
          ebx = 0;
          edi = cursorId;                                 // <=
          // Window event WE_UNKNOWN_0E was called here,
          // but no windows actually implemented a handler and
          // it's not known what it was for
          cursorId = edi;                                 // <=
          if ((ebx & 0xFF) != 0)
          {
            set_cursor(cursorId);
            return;
          }
          break;
          ....
      }
      ....
    }

    This code fragment was most likely obtained by decompilation. Then, judging by the comment left, a part of the non-working code was deleted. However, there is still a couple of operations on cursorId that also don't make much sense.

    Warning N6

    V1004 [CWE-476] The 'player' pointer was used unsafely after it was verified against nullptr. Check lines: 2085, 2094. libopenrct2 Network.cpp 2094

    void Network::ProcessPlayerList()
    {
      ....
      auto* player = GetPlayerByID(pendingPlayer.Id);
      if (player == nullptr)
      {
        // Add new player.
        player = AddPlayer("", "");
        if (player)                                          // <=
        {
          *player = pendingPlayer;
           if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)
           {
             _serverConnection->Player = player;
           }
        }
        newPlayers.push_back(player->Id);                    // <=
      }
      ....
    }

    This code is quite simple to correct — one either needs to check player for a null pointer for the third time, or add it to the body of the conditional operator. I would suggest the second option:

    void Network::ProcessPlayerList()
    {
      ....
      auto* player = GetPlayerByID(pendingPlayer.Id);
      if (player == nullptr)
      {
        // Add new player.
        player = AddPlayer("", "");
        if (player)
        {
          *player = pendingPlayer;
          if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)
          {
            _serverConnection->Player = player;
          }
          newPlayers.push_back(player->Id);
        }
      }
      ....
    }

    Warning N7

    V547 [CWE-570] Expression 'name == nullptr' is always false. libopenrct2 ServerList.cpp 102

    std::optional<ServerListEntry> ServerListEntry::FromJson(...)
    {
      auto name = json_object_get(server, "name");
      .....
      if (name == nullptr || version == nullptr)
      {
        ....
      }
      else
      {
        ....
        entry.name = (name == nullptr ? "" : json_string_value(name));
        ....
      }
      ....
    }

    You can get rid of a hard-to-read line of code in one fell swoop and solve the problem with checking for nullptr. I would change the code as follows:

    std::optional<ServerListEntry> ServerListEntry::FromJson(...)
    {
      auto name = json_object_get(server, "name");
      .....
      if (name == nullptr || version == nullptr)
      {
        name = ""
        ....
      }
      else
      {
        ....
        entry.name = json_string_value(name);
        ....
      }
      ....
    }

    Warning N8

    V1048 [CWE-1164] The 'ColumnHeaderPressedCurrentState' variable was assigned the same value. libopenrct2ui CustomListView.cpp 510

    void CustomListView::MouseUp(....)
    {
      ....
      if (!ColumnHeaderPressedCurrentState)
      {
        ColumnHeaderPressed = std::nullopt;
        ColumnHeaderPressedCurrentState = false;
        Invalidate();
      }
    }

    The code looks quite strange. I think there was a typo either in the condition or when reassigning the false value to the ColumnHeaderPressedCurrentStatevariable.

    Conclusion


    As we can see, it is quite easy to integrate the PVS-Studio static analyzer into your TeamCity project. To do this, you just need to write one small configuration file. For its part, checking the code will allow you to detect problems immediately after the build, which will help you fix them when the complexity and cost of edits are still small.
    PVS-Studio
    Static Code Analysis for C, C++, C# and Java

    Comments 0

    Only users with full accounts can post comments. Log in, please.