]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10125 Add project relative path for all components
authorJanos Gyerik <janos.gyerik@sonarsource.com>
Fri, 1 Dec 2017 11:01:27 +0000 (12:01 +0100)
committerJanos Gyerik <janos.gyerik@sonarsource.com>
Tue, 5 Dec 2017 09:47:46 +0000 (10:47 +0100)
sonar-plugin-api/src/main/java/org/sonar/api/batch/fs/internal/DefaultInputDir.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/fs/internal/TestInputFileBuilder.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ComponentsPublisher.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ComponentsPublisherTest.java
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto

index 79031632b08e8c408a6ceefdb57a7a94b5230eeb..0acef07079c8c5af7475c403e596b34355f7be0c 100644 (file)
@@ -84,7 +84,7 @@ public class DefaultInputDir extends DefaultInputComponent implements InputDir {
   }
 
   /**
-   * For testing purpose. Will be automaticall set when dir is added to {@link DefaultFileSystem}
+   * For testing purpose. Will be automatically set when dir is added to {@link DefaultFileSystem}
    */
   public DefaultInputDir setModuleBaseDir(Path moduleBaseDir) {
     this.moduleBaseDir = moduleBaseDir.normalize();
index 671790bdf436f9614452ddcc1ef2ef4dc2bf422e..3d93259b8b3835716f1a0b0d5a03fd70b912bc7a 100644 (file)
@@ -23,9 +23,11 @@ import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
 import java.nio.charset.Charset;
+import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.batch.bootstrap.ProjectDefinition;
 import org.sonar.api.batch.fs.InputFile;
@@ -55,6 +57,8 @@ public class TestInputFileBuilder {
   private final int id;
   private final String relativePath;
   private final String moduleKey;
+  @CheckForNull
+  private Path projectBaseDir;
   private Path moduleBaseDir;
   private String language;
   private InputFile.Type type = InputFile.Type.MAIN;
@@ -77,7 +81,7 @@ public class TestInputFileBuilder {
   }
 
   /**
-   * Create a InputFile with a given module key and module base directory. 
+   * Create a InputFile with a given module key and module base directory.
    * The relative path is generated comparing the file path to the module base directory. 
    * filePath must point to a file that is within the module base directory.
    */
@@ -108,13 +112,22 @@ public class TestInputFileBuilder {
     return batchId++;
   }
 
+  public TestInputFileBuilder setProjectBaseDir(Path projectBaseDir) {
+    this.projectBaseDir = normalize(projectBaseDir);
+    return this;
+  }
+
   public TestInputFileBuilder setModuleBaseDir(Path moduleBaseDir) {
+    this.moduleBaseDir = normalize(moduleBaseDir);
+    return this;
+  }
+
+  private static Path normalize(Path path) {
     try {
-      this.moduleBaseDir = moduleBaseDir.normalize().toRealPath(LinkOption.NOFOLLOW_LINKS);
+      return path.normalize().toRealPath(LinkOption.NOFOLLOW_LINKS);
     } catch (IOException e) {
-      this.moduleBaseDir = moduleBaseDir.normalize();
+      return path.normalize();
     }
-    return this;
   }
 
   public TestInputFileBuilder setLanguage(@Nullable String language) {
@@ -192,7 +205,12 @@ public class TestInputFileBuilder {
   }
 
   public DefaultInputFile build() {
-    DefaultIndexedFile indexedFile = new DefaultIndexedFile(moduleBaseDir.resolve(relativePath), moduleKey, relativePath, relativePath, type, language, id, new SensorStrategy());
+    Path absolutePath = moduleBaseDir.resolve(relativePath);
+    if (projectBaseDir == null) {
+      projectBaseDir = moduleBaseDir;
+    }
+    String projectRelativePath = projectBaseDir.relativize(absolutePath).toString();
+    DefaultIndexedFile indexedFile = new DefaultIndexedFile(absolutePath, moduleKey, projectRelativePath, relativePath, type, language, id, new SensorStrategy());
     DefaultInputFile inputFile = new DefaultInputFile(indexedFile,
       f -> f.setMetadata(new Metadata(lines, nonBlankLines, hash, originalLineOffsets, lastValidOffset)),
       contents);
@@ -203,11 +221,35 @@ public class TestInputFileBuilder {
   }
 
   public static DefaultInputModule newDefaultInputModule(String moduleKey, File baseDir) {
-    ProjectDefinition definition = ProjectDefinition.create().setKey(moduleKey).setBaseDir(baseDir).setWorkDir(new File(baseDir, ".sonar"));
+    ProjectDefinition definition = ProjectDefinition.create()
+      .setKey(moduleKey)
+      .setBaseDir(baseDir)
+      .setWorkDir(new File(baseDir, ".sonar"));
     return newDefaultInputModule(definition);
   }
 
   public static DefaultInputModule newDefaultInputModule(ProjectDefinition projectDefinition) {
     return new DefaultInputModule(projectDefinition, TestInputFileBuilder.nextBatchId());
   }
+
+  public static DefaultInputModule newDefaultInputModule(DefaultInputModule parent, String key) throws IOException {
+    Path basedir = parent.getBaseDir().resolve(key);
+    Files.createDirectory(basedir);
+    return newDefaultInputModule(key, basedir.toFile());
+  }
+
+  public static DefaultInputDir newDefaultInputDir(DefaultInputModule module, String relativePath) throws IOException {
+    Path basedir = module.getBaseDir().resolve(relativePath);
+    Files.createDirectory(basedir);
+    return new DefaultInputDir(module.key(), relativePath)
+      .setModuleBaseDir(module.getBaseDir());
+  }
+
+  public static DefaultInputFile newDefaultInputFile(Path projectBaseDir, DefaultInputModule module, String relativePath) {
+    return new TestInputFileBuilder(module.key(), relativePath)
+      .setStatus(InputFile.Status.SAME)
+      .setProjectBaseDir(projectBaseDir)
+      .setModuleBaseDir(module.getBaseDir())
+      .build();
+  }
 }
index 9863ba0cdba591b63089e1bdc0d2ff9f0fd6d8ac..0426048478b59a547540218e8464ee1f17957e3c 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.scanner.report;
 
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.stream.Collectors;
 import javax.annotation.CheckForNull;
@@ -122,6 +123,11 @@ public class ComponentsPublisher implements ReportPublisherStep {
     String path = getPath(component);
     if (path != null) {
       builder.setPath(path);
+
+      String projectRelativePath = getProjectRelativePath(component);
+      if (projectRelativePath != null) {
+        builder.setProjectRelativePath(projectRelativePath);
+      }
     }
 
     for (InputComponent child : children) {
@@ -187,7 +193,26 @@ public class ComponentsPublisher implements ReportPublisherStep {
       InputModule module = (InputModule) component;
       return moduleHierarchy.relativePath(module);
     }
-    throw new IllegalStateException("Unkown component: " + component.getClass());
+    throw new IllegalStateException("Unknown component: " + component.getClass());
+  }
+
+  @CheckForNull
+  private String getProjectRelativePath(DefaultInputComponent component) {
+    if (component instanceof InputFile) {
+      DefaultInputFile inputFile = (DefaultInputFile) component;
+      return inputFile.getProjectRelativePath();
+    }
+
+    Path projectBaseDir = moduleHierarchy.root().getBaseDir();
+    if (component instanceof InputDir) {
+      InputDir inputDir = (InputDir) component;
+      return projectBaseDir.relativize(inputDir.path()).toString();
+    }
+    if (component instanceof InputModule) {
+      DefaultInputModule module = (DefaultInputModule) component;
+      return projectBaseDir.relativize(module.getBaseDir()).toString();
+    }
+    throw new IllegalStateException("Unknown component: " + component.getClass());
   }
 
   private String getVersion(DefaultInputModule module) {
index 1f017e7ada98068302b9b2e07fb4f199b7698409..51922d73adefcedee42f55e70cc98463ac5caaa0 100644 (file)
@@ -21,8 +21,12 @@ package org.sonar.scanner.report;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -45,13 +49,15 @@ import org.sonar.scanner.protocol.output.ScannerReport.Component.FileStatus;
 import org.sonar.scanner.protocol.output.ScannerReport.ComponentLink.ComponentLinkType;
 import org.sonar.scanner.protocol.output.ScannerReportReader;
 import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.scan.DefaultComponentTree;
+import org.sonar.scanner.scan.DefaultInputModuleHierarchy;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
 import org.sonar.scanner.scan.branch.BranchType;
-import org.sonar.scanner.scan.DefaultComponentTree;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.sonar.api.batch.fs.internal.TestInputFileBuilder.*;
 
 public class ComponentsPublisherTest {
   @Rule
@@ -91,11 +97,12 @@ public class ComponentsPublisherTest {
       .setWorkDir(temp.newFolder());
     DefaultInputModule root = new DefaultInputModule(rootDef, 1);
 
+    Path moduleBaseDir = temp.newFolder().toPath();
     ProjectDefinition module1Def = ProjectDefinition.create()
       .setKey("module1")
       .setName("Module1")
       .setDescription("Module description")
-      .setBaseDir(temp.newFolder())
+      .setBaseDir(moduleBaseDir.toFile())
       .setWorkDir(temp.newFolder());
     rootDef.addSubProject(module1Def);
 
@@ -107,12 +114,20 @@ public class ComponentsPublisherTest {
     when(moduleHierarchy.parent(module1)).thenReturn(root);
     tree.index(module1, root);
 
-    DefaultInputDir dir = new DefaultInputDir("module1", "src", 3);
+    DefaultInputDir dir = new DefaultInputDir("module1", "src", 3)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir, module1);
 
+    DefaultInputDir dir2 = new DefaultInputDir("module1", "src2", 17)
+      .setModuleBaseDir(moduleBaseDir);
+    tree.index(dir2, module1);
+
     DefaultInputFile file = new TestInputFileBuilder("module1", "src/Foo.java", 4).setLines(2).setStatus(InputFile.Status.SAME).build();
     tree.index(file, dir);
 
+    DefaultInputFile file18 = new TestInputFileBuilder("module1", "src2/Foo.java", 18).setLines(2).setStatus(InputFile.Status.SAME).build();
+    tree.index(file18, dir2);
+
     DefaultInputFile file2 = new TestInputFileBuilder("module1", "src/Foo2.java", 5).setPublish(false).setLines(2).build();
     tree.index(file2, dir);
 
@@ -183,12 +198,13 @@ public class ComponentsPublisherTest {
     ProjectAnalysisInfo projectAnalysisInfo = mock(ProjectAnalysisInfo.class);
     when(projectAnalysisInfo.analysisDate()).thenReturn(DateUtils.parseDate("2012-12-12"));
 
+    Path moduleBaseDir = temp.newFolder().toPath();
     ProjectDefinition rootDef = ProjectDefinition.create()
       .setKey("foo")
       .setProperty(CoreProperties.PROJECT_VERSION_PROPERTY, "1.0")
       .setName("Root project")
       .setDescription("Root description")
-      .setBaseDir(temp.newFolder())
+      .setBaseDir(moduleBaseDir.toFile())
       .setWorkDir(temp.newFolder());
     DefaultInputModule root = new DefaultInputModule(rootDef, 1);
 
@@ -197,15 +213,18 @@ public class ComponentsPublisherTest {
     when(moduleHierarchy.children(root)).thenReturn(Collections.emptyList());
 
     // dir with files
-    DefaultInputDir dir = new DefaultInputDir("module1", "src", 2);
+    DefaultInputDir dir = new DefaultInputDir("module1", "src", 2)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir, root);
 
     // dir without files and issues
-    DefaultInputDir dir2 = new DefaultInputDir("module1", "src2", 3);
+    DefaultInputDir dir2 = new DefaultInputDir("module1", "src2", 3)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir2, root);
 
     // dir without files but has issues
-    DefaultInputDir dir3 = new DefaultInputDir("module1", "src3", 4);
+    DefaultInputDir dir3 = new DefaultInputDir("module1", "src3", 4)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir3, root);
     writeIssue(4);
 
@@ -294,12 +313,13 @@ public class ComponentsPublisherTest {
     ProjectAnalysisInfo projectAnalysisInfo = mock(ProjectAnalysisInfo.class);
     when(projectAnalysisInfo.analysisDate()).thenReturn(DateUtils.parseDate("2012-12-12"));
 
+    Path moduleBaseDir = temp.newFolder().toPath();
     ProjectDefinition rootDef = ProjectDefinition.create()
       .setKey("foo")
       .setProperty(CoreProperties.PROJECT_VERSION_PROPERTY, "1.0")
       .setName("Root project")
       .setDescription("Root description")
-      .setBaseDir(temp.newFolder())
+      .setBaseDir(moduleBaseDir.toFile())
       .setWorkDir(temp.newFolder());
     DefaultInputModule root = new DefaultInputModule(rootDef, 1);
 
@@ -308,15 +328,18 @@ public class ComponentsPublisherTest {
     when(moduleHierarchy.children(root)).thenReturn(Collections.emptyList());
 
     // dir with changed files
-    DefaultInputDir dir = new DefaultInputDir("module1", "src", 2);
+    DefaultInputDir dir = new DefaultInputDir("module1", "src", 2)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir, root);
 
     // dir without changed files or issues
-    DefaultInputDir dir2 = new DefaultInputDir("module1", "src2", 3);
+    DefaultInputDir dir2 = new DefaultInputDir("module1", "src2", 3)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir2, root);
 
     // dir without changed files but has issues
-    DefaultInputDir dir3 = new DefaultInputDir("module1", "src3", 4);
+    DefaultInputDir dir3 = new DefaultInputDir("module1", "src3", 4)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir3, root);
     writeIssue(4);
 
@@ -366,10 +389,11 @@ public class ComponentsPublisherTest {
       .setWorkDir(temp.newFolder());
     DefaultInputModule root = new DefaultInputModule(rootDef, 1);
 
+    Path moduleBaseDir = temp.newFolder().toPath();
     ProjectDefinition module1Def = ProjectDefinition.create()
       .setKey("module1")
       .setDescription("Module description")
-      .setBaseDir(temp.newFolder())
+      .setBaseDir(moduleBaseDir.toFile())
       .setWorkDir(temp.newFolder());
     rootDef.addSubProject(module1Def);
     DefaultInputModule module1 = new DefaultInputModule(module1Def, 2);
@@ -379,7 +403,8 @@ public class ComponentsPublisherTest {
     when(moduleHierarchy.children(root)).thenReturn(Collections.singleton(module1));
     tree.index(module1, root);
 
-    DefaultInputDir dir = new DefaultInputDir("module1", "src", 3);
+    DefaultInputDir dir = new DefaultInputDir("module1", "src", 3)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir, module1);
 
     DefaultInputFile file = new TestInputFileBuilder("module1", "src/Foo.java", 4).setLines(2).setStatus(InputFile.Status.SAME).build();
@@ -435,12 +460,13 @@ public class ComponentsPublisherTest {
       .setWorkDir(temp.newFolder());
     DefaultInputModule root = new DefaultInputModule(rootDef, 1);
 
+    Path moduleBaseDir = temp.newFolder().toPath();
     ProjectDefinition module1Def = ProjectDefinition.create()
       .setKey("module1")
       .setName("Module1")
       .setProperty(CoreProperties.LINKS_CI, "http://ci")
       .setDescription("Module description")
-      .setBaseDir(temp.newFolder())
+      .setBaseDir(moduleBaseDir.toFile())
       .setWorkDir(temp.newFolder());
     rootDef.addSubProject(module1Def);
     DefaultInputModule module1 = new DefaultInputModule(module1Def, 2);
@@ -451,7 +477,8 @@ public class ComponentsPublisherTest {
     when(moduleHierarchy.parent(module1)).thenReturn(root);
     tree.index(module1, root);
 
-    DefaultInputDir dir = new DefaultInputDir("module1", "src", 3);
+    DefaultInputDir dir = new DefaultInputDir("module1", "src", 3)
+      .setModuleBaseDir(moduleBaseDir);
     tree.index(dir, module1);
 
     DefaultInputFile file = new TestInputFileBuilder("module1", "src/Foo.java", 4).setLines(2).setStatus(InputFile.Status.SAME).build();
@@ -473,4 +500,111 @@ public class ComponentsPublisherTest {
     assertThat(module1Protobuf.getLink(0).getType()).isEqualTo(ComponentLinkType.CI);
     assertThat(module1Protobuf.getLink(0).getHref()).isEqualTo("http://ci");
   }
+
+  @Test
+  public void add_components_with_correct_project_relative_path() throws Exception {
+    Map<DefaultInputModule, DefaultInputModule> parents = new HashMap<>();
+
+    DefaultInputModule root = newDefaultInputModule("foo", temp.newFolder());
+
+    DefaultInputFile file = newDefaultInputFile(root.getBaseDir(), root, "Foo.java");
+    tree.index(file, root);
+
+    DefaultInputDir dir1 = newDefaultInputDir(root, "dir1");
+    tree.index(dir1, root);
+
+    DefaultInputFile dir1_file = newDefaultInputFile(root.getBaseDir(), root, "dir1/Foo.java");
+    tree.index(dir1_file, dir1);
+
+    DefaultInputDir dir1_dir1 = newDefaultInputDir(root, "dir1/dir1");
+    tree.index(dir1_dir1, dir1);
+
+    DefaultInputFile dir1_dir1_file = newDefaultInputFile(root.getBaseDir(), root, "dir1/dir1/Foo.java");
+    tree.index(dir1_dir1_file, dir1_dir1);
+
+    // module in root
+
+    DefaultInputModule mod1 = newDefaultInputModule(root, "mod1");
+    parents.put(mod1, root);
+    tree.index(mod1, root);
+
+    DefaultInputFile mod1_file = newDefaultInputFile(root.getBaseDir(), mod1, "Foo.java");
+    tree.index(mod1_file, mod1);
+
+    DefaultInputDir mod1_dir2 = newDefaultInputDir(mod1, "dir2");
+    tree.index(mod1_dir2, mod1);
+
+    DefaultInputFile mod1_dir2_file = newDefaultInputFile(root.getBaseDir(), mod1, "dir2/Foo.java");
+    tree.index(mod1_dir2_file, mod1_dir2);
+
+    // module in module
+
+    DefaultInputModule mod1_mod2 = newDefaultInputModule(mod1, "mod2");
+    parents.put(mod1_mod2, mod1);
+    tree.index(mod1_mod2, mod1);
+
+    DefaultInputFile mod1_mod2_file = newDefaultInputFile(root.getBaseDir(), mod1_mod2, "Foo.java");
+    tree.index(mod1_mod2_file, mod1_mod2);
+
+    DefaultInputDir mod1_mod2_dir = newDefaultInputDir(mod1_mod2, "dir");
+    tree.index(mod1_mod2_dir, mod1_mod2);
+
+    DefaultInputFile mod1_mod2_dir_file = newDefaultInputFile(root.getBaseDir(), mod1_mod2, "dir/Foo.java");
+    tree.index(mod1_mod2_dir_file, mod1_mod2_dir);
+
+    moduleHierarchy = new DefaultInputModuleHierarchy(parents);
+
+    ComponentsPublisher publisher = new ComponentsPublisher(moduleHierarchy, tree, branchConfiguration);
+    publisher.publish(writer);
+
+    ScannerReportReader reader = new ScannerReportReader(outputDir);
+
+    // project root
+    assertThat(reader.readComponent(root.batchId()).getPath()).isEmpty();
+    assertThat(reader.readComponent(root.batchId()).getProjectRelativePath()).isEmpty();
+
+    // file in root
+    assertThat(reader.readComponent(file.batchId()).getPath()).isEqualTo("Foo.java");
+    assertThat(reader.readComponent(file.batchId()).getProjectRelativePath()).isEqualTo("Foo.java");
+
+    // dir in root
+    assertThat(reader.readComponent(dir1.batchId()).getPath()).isEqualTo("dir1");
+    assertThat(reader.readComponent(dir1.batchId()).getProjectRelativePath()).isEqualTo("dir1");
+
+    // file in dir in root
+    assertThat(reader.readComponent(dir1_file.batchId()).getPath()).isEqualTo("dir1/Foo.java");
+    assertThat(reader.readComponent(dir1_file.batchId()).getProjectRelativePath()).isEqualTo("dir1/Foo.java");
+
+    // dir in dir in root
+    assertThat(reader.readComponent(dir1_dir1.batchId()).getPath()).isEqualTo("dir1/dir1");
+    assertThat(reader.readComponent(dir1_dir1.batchId()).getProjectRelativePath()).isEqualTo("dir1/dir1");
+
+    // module in root
+    assertThat(reader.readComponent(mod1.batchId()).getPath()).isEqualTo("mod1");
+    assertThat(reader.readComponent(mod1.batchId()).getProjectRelativePath()).isEqualTo("mod1");
+
+    // dir in module in root
+    assertThat(reader.readComponent(mod1_dir2.batchId()).getPath()).isEqualTo("dir2");
+    assertThat(reader.readComponent(mod1_dir2.batchId()).getProjectRelativePath()).isEqualTo("mod1/dir2");
+
+    // file in dir in module in root
+    assertThat(reader.readComponent(mod1_dir2_file.batchId()).getPath()).isEqualTo("dir2/Foo.java");
+    assertThat(reader.readComponent(mod1_dir2_file.batchId()).getProjectRelativePath()).isEqualTo("mod1/dir2/Foo.java");
+
+    // module in module
+    assertThat(reader.readComponent(mod1_mod2.batchId()).getPath()).isEqualTo("mod2");
+    assertThat(reader.readComponent(mod1_mod2.batchId()).getProjectRelativePath()).isEqualTo("mod1/mod2");
+
+    // file in module in module
+    assertThat(reader.readComponent(mod1_mod2_file.batchId()).getPath()).isEqualTo("Foo.java");
+    assertThat(reader.readComponent(mod1_mod2_file.batchId()).getProjectRelativePath()).isEqualTo("mod1/mod2/Foo.java");
+
+    // dir in module in module
+    assertThat(reader.readComponent(mod1_mod2_dir.batchId()).getPath()).isEqualTo("dir");
+    assertThat(reader.readComponent(mod1_mod2_dir.batchId()).getProjectRelativePath()).isEqualTo("mod1/mod2/dir");
+
+    // file in dir in module in module
+    assertThat(reader.readComponent(mod1_mod2_dir_file.batchId()).getPath()).isEqualTo("dir/Foo.java");
+    assertThat(reader.readComponent(mod1_mod2_dir_file.batchId()).getProjectRelativePath()).isEqualTo("mod1/mod2/dir/Foo.java");
+  }
 }
index e939fa8855ec6dac8ef7691c80a9e7a8db5d1de0..75f915b9501577c9b0f0534038456135a857edca 100644 (file)
@@ -94,6 +94,8 @@ message ComponentLink {
 
 message Component {
   int32 ref = 1;
+
+  // Path relative to module base directory
   string path = 2;
   string name = 3;
   ComponentType type = 4;
@@ -111,6 +113,9 @@ message Component {
   // Only available on PROJECT and MODULE types
   string description = 12;
   FileStatus status = 13;
+
+  // Path relative to project base directory
+  string project_relative_path = 14;
   
        enum ComponentType {
          UNSET = 0;