Uploaded image for project: 'Minimal Marketable Features'
  1. Minimal Marketable Features
  2. MMF-1386

SonarLint for IntelliJ - Drop the concept of module

    Details

    • Type: MMF
    • Status: Closed
    • Priority: Major
    • Resolution: Fixed
    • Labels:

      Description

      WHY

      As described in MMF-365, we’ve decided to stop supporting modules in our ecosystem: SonarQube, SonarCloud and SonarLint.
      With this MMF, we’ll do a first step towards this end. Indeed, SonarLint must be ready to properly work without modules before those ones disappear from SonarQube and SonarCloud.

      Also, it will resolve several problems/limitations we have with the support of modules in SonarLint:

      For a multi modules project, SonarLint for IntelliJ retrieves from SonarQube/SonarCloud the structure of modules as well as the files paths relatively to those modules. From there, it extrapolates the paths of remote files relatively to the project root.
      This logic works only in cases where the structure of the project in IntelliJ perfectly matches the hierachy of the project in SonarQube.
      And that’s not the case when:

      • the local filesystem structure doesn’t match with the modules hierarchy as known by the server,
      • the user opens a module of a project instead of the project root,
      • the user opens a project analyzed in SonarQube as a module in IntellIJ,
      • the user adds files to the project that are not within the base directory.

      In all these cases, SonarLint is not be able to map local files to the remote ones and, as a consequence, it fails to:

      • hide issues that are marked as won’t fix /false positive
      • display the same issue type, severity, creation date than on server side
      • apply exclusions defined on the Sonarqube

      In SonarLint for Eclipse, this logic to deal with the modules structure doesn’t exist. For a multi-modules project, the user has to manually bind each project module to the module in SonarQube for the synchronization of issues to work properly.

      WHAT

      From the user point of view, the change should be as much as possible seamless:

      • After the upgrade, when the user opens a project with modules, SonarLint will invalidate the storage (based on the version if was build for) and will require an update of the binding.
      • From there, SonarLint should be able fetch the list of remote files and to use an heuristic approach (relying for example on the longest file suffix) to map local files with the ones in SonarQube.

      We don't expect any change in term of user experience.
      And we need, of course, to keep supporting modules for the case SonarLint connects to a version of SonarQube where modules are still present.

      Question to be answered with the HOW:

      • What is exactly the logic we can apply to map local files with the remote ones?
      • Without implementing a mechanism to automatically find the project on SonarQube, can we avoid selecting the remote project for each module?

      HOW

      Reminder of existing behavior in SLI

      Get the list of modules in the project with: /api/components/tree?qualifiers=BRC&baseComponentKey=org.sonarsource.sonarlint.core%3Asonarlint-core-parent

      {
        "baseComponent": {
          "id": "AVKDMaWbprNVR5Ye2J12",
          "key": "org.sonarsource.sonarlint.core:sonarlint-core-parent",
          "name": "SonarLint Core",
          "qualifier": "TRK"
        },
        "components": [
          {
            "id": "AVLQ6Scdt7Anf4a6KdAh",
            "key": "org.sonarsource.sonarlint.core:sonarlint-client-api",
            "name": "SonarLint Core - Client API",
            "qualifier": "BRC",
            "path": "client-api"
          },
          {
            "id": "AVLQ6Scft7Anf4a6KdDI",
            "key": "org.sonarsource.sonarlint.core:sonarlint-core",
            "name": "SonarLint Core - Implementation",
            "qualifier": "BRC",
            "path": "core"
          },
          {
            "id": "AVRWmDhiki6HeiaYtPmr",
            "key": "org.sonarsource.sonarlint.core:sonarlint-daemon",
            "name": "SonarLint Daemon",
            "qualifier": "BRC",
            "path": "daemon"
          },
          {
            "id": "AVRWmDhiki6HeiaYtPms",
            "key": "org.sonarsource.sonarlint.core:sonarlint-daemon-protocol",
            "name": "SonarLint Daemon Protocol",
            "qualifier": "BRC",
            "path": "daemon-protocol"
          },
          {
            "id": "AVw1P3ypxV3sqrYcQkc2",
            "key": "org.sonarsource.sonarlint.core:sonarlint-language-server",
            "name": "SonarLint Language Server",
            "qualifier": "BRC",
            "path": "language-server"
          }
        ]
      }
      

      Then for each module, get its parent with: api/components/show?id=AVLQ6Scdt7Anf4a6KdAh

      {
        "component": {
          "id": "AVLQ6Scdt7Anf4a6KdAh",
          "key": "org.sonarsource.sonarlint.core:sonarlint-client-api",
          "name": "SonarLint Core - Client API",
          "qualifier": "BRC",
          "path": "client-api"
        },
        "ancestors": [
          {
            "id": "AVKDMaWbprNVR5Ye2J12",
            "key": "org.sonarsource.sonarlint.core:sonarlint-core-parent",
            "name": "SonarLint Core",
            "qualifier": "TRK"
          }
        ]
      }
      

      It allows to build a map of <modules key, module relative path from root>. For example here:

      • "org.sonarsource.sonarlint.core:sonarlint-core-parent" -> ""
      • "org.sonarsource.sonarlint.core:sonarlint-client-api" -> "client-api"
      • "org.sonarsource.sonarlint.core:sonarlint-core" -> "core"

      Then for any file in an IntelliJ project, here is the process (TBC):

      • get the relative path of the IntelliJ module inside the IntelliJ workspace (for example, /client-api)
      • from this, read the dictionnary to find module key -> org.sonarsource.sonarlint.core:sonarlint-client-api
      • append the file relative path to form the file key -> org.sonarsource.sonarlint.core:sonarlint-client-api:src/main/java/Foo.java
      • read the issue storage

      Emulate relative project paths for SQ < 7.5 (assuming modules will be dropped in 7.4)

      From now on, when accessing the store of issues, we use the relative path of the file from the root project's base dir.
      For projects that still have modules, this relative path to the root project base dir needs to be rebuilt before reading or writing issues.
      Here is out it will work when accessing a project that still contains modules.

      1) Get the list of all files in the project.
      api/components/tree?qualifiers=FIL&baseComponentKey=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&strategy=leaves

      {
        "components": [
          {
            "id": "AVLQ6Scet7Anf4a6KdBY",
            "key":
              "org.sonarsource.sonarlint.core:sonarlint-core:src/main/java/org/sonarsource/sonarlint/core/container/analysis/filesystem/AbsolutePathPredicate.java",
            "qualifier": "FIL",
            "path":
              "src/main/java/org/sonarsource/sonarlint/core/container/analysis/filesystem/AbsolutePathPredicate.java"
          },
          {
            "id": "AVLQ6Scet7Anf4a6KdBZ",
            "key":
              "org.sonarsource.sonarlint.core:sonarlint-core:src/main/java/org/sonarsource/sonarlint/core/container/analysis/filesystem/AbstractFilePredicate.java",
            "qualifier": "FIL",
            "path":
              "src/main/java/org/sonarsource/sonarlint/core/container/analysis/filesystem/AbstractFilePredicate.java"
          },
          {
            "id": "AVOebP6-1iu22sbktsHc",
            "key":
              "org.sonarsource.sonarlint.core:sonarlint-client-api:src/main/java/org/sonarsource/sonarlint/core/client/api/common/AbstractGlobalConfiguration.java",
            "qualifier": "FIL",
            "path":
              "src/main/java/org/sonarsource/sonarlint/core/client/api/common/AbstractGlobalConfiguration.java"
          },
          [...]
        ]
      }
      

      2) Since file path is relative to their module, rebuild the path relative to the project (using the previous dictionary).

      • "org.sonarsource.sonarlint.core:sonarlint-core:src/main/java/org/sonarsource/sonarlint/core/container/analysis/filesystem/AbsolutePathPredicate.java" -> "core/src/main/java/org/sonarsource/sonarlint/core/container/analysis/filesystem/AbsolutePathPredicate.java"
      • "org.sonarsource.sonarlint.core:sonarlint-client-api:src/main/java/org/sonarsource/sonarlint/core/client/api/common/AbstractGlobalConfiguration.java" -> "client-api/src/main/java/org/sonarsource/sonarlint/core/client/api/common/AbstractGlobalConfiguration.java"

      Note that this process will also work with SQ 7.5+, assuming the WS are backward compatible (simply, there will be no modules, and all file paths will already be relative the the project root)

      File matching

      SonarLint needs to figure out what is the SonarQube's relative path for each file imported in the IDE, belonging to a module that is bound to a SonarQube project.

      On one side, we have a list of all files imported in the IDE for a given module. For each file, we have the IDE's module relative path.
      On the other side, we have the relative paths (from the project base dir) of all files in the SonarQube project.

      Algorithm

      1. Take the two sets of files (IDE files and SQ files);
      2. File relative paths are split into its elements;
      3. For each file in SQ, we find the longest suffix match in the IDE's side (if any). This results possibly in an unmatched prefix in both the IDE and SQ sides.
      4. We check what is the pair of prefixes (in IDE and SQ) with which the highest number of files are matched;
      5. The SQ file key of each local file is: moduleKey:<SQ_prefix>/relative_module_path_after_IDE_prefix. Files that have a relative path not starting with the IDE_prefix are unmatched;

      Example

      1. IDE's and SQ's files

      SQ:

      project1/pom.xml
      project1/moduleA/pom.xml
      project1/moduleA/src/Foo.java
      project1/moduleA/src/package-info.java
      project1/moduleB/pom.xml
      project1/moduleB/src/Duplicated.java
      project1/moduleB/src/package-info.java
      project2/pom.xml
      

      IDE:

      project1-trunk/pom.xml
      project1-trunk/moduleA/pom.xml
      project1-trunk/moduleA/src/Foo.java
      project1-trunk/moduleA/src/Duplicated.java
      project1-trunk/moduleA/src/package-info.java
      project1-trunk/moduleB/pom.xml
      project1-trunk/moduleB/src/Duplicated.java
      project1-trunk/moduleB/src/package-info.java
      project1-trunk/moduleB/Duplicated.java
      project2-trunk/File.java
      

      2. Simply split each path into its elements.

      3. For example:
      project1/moduleA/src/Foo.java will have the suffix match moduleA/src/Foo.java. As a result, we have the IDE prefix project1-trunk and the SQ prefix project1.

      4. In this case, it will be project1-trunk / project1.

      5. In the IDE, the file project1-trunk/pom.xml will have the key sqModule:project1/pom.xml. The file project2/File.java is unmatched.

        Attachments

          Issue Links

            Activity

              People

              • Assignee:
                christophe.levis Christophe Levis
                Reporter:
                christophe.levis Christophe Levis
              • Votes:
                0 Vote for this issue
                Watchers:
                1 Start watching this issue

                Dates

                • Created:
                  Updated:
                  Resolved: