Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix IDETECT-4533: Update project name mechanism to avoid infinite loop #1282

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public Extraction extract(File directory, ExecutableTarget gradleExe, @Nullable
List<File> reportFiles = fileFinder.findFiles(outputDirectory,"*_dependencyGraph.txt");
List<CodeLocation> codeLocations = new ArrayList<>();

//Get all the extraction files and sort all the files by depth number in ascending order starting from root to the deepest submodule,
// this will help in getting all the rich versions for parents, and then we move on to parse child submodules to see usage of those files
File[] files = new File[reportFiles.size()];
reportFiles.toArray(files);
List<File> reportFilesSorted = Arrays.asList(sortFilesByDepth(files));
Expand Down Expand Up @@ -99,6 +101,7 @@ private Optional<NameVersion> parseRootProjectMetadataFile(File rootProjectMetad
}
}

// Sort all the files containing dependency extractions in ascending-order
private File[] sortFilesByDepth(File[] files) {
Arrays.sort(files, new Comparator<File>() {
@Override
Expand All @@ -111,7 +114,7 @@ public int compare(File o1, File o2) {
private int extractDepthNumber(String name) {
int i;
try {
int s = name.indexOf("depth") + 5;
int s = name.indexOf("depth") + 5; // File name is like project__projectname__depth3_dependencyGraph.txt, we extract the number after depth
int e = name.indexOf("_dependencyGraph");
String number = name.substring(s, e);
i = Integer.parseInt(number);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,19 @@ public class GradleReportLineParser {
// This map is handling all the child-parent relationships which are found in the entire project
// with each child as a key and their respective parent as the value.
private final Map<String, String> relationsMap = new HashMap<>();
// tracks the project where rich version was declared
private String richVersionProject = null;
// tracks if rich version was declared while parsing the previous line
private boolean richVersionDeclared = false;
private String projectName;
private String rootProjectName;
private String projectParent;
private int level;
private String depthNumber;
public static final String PROJECT_NAME_PREFIX = "projectName:";
public static final String ROOT_PROJECT_NAME_PREFIX = "rootProjectName:";
public static final String PROJECT_PARENT_PREFIX = "projectParent:";
public static final String FILE_NAME_PREFIX = "fileName:";

public GradleTreeNode parseLine(String line, Map<String, String> metadata) {
level = parseTreeLevel(line);
Expand Down Expand Up @@ -109,15 +113,29 @@ private List<String> parseGav(String line, Map<String, String> metadata) {
}
}

projectName = metadata.getOrDefault(PROJECT_NAME_PREFIX, "orphanProject");
rootProjectName = metadata.getOrDefault(ROOT_PROJECT_NAME_PREFIX, "");
projectParent = metadata.getOrDefault(PROJECT_PARENT_PREFIX, "null");
projectName = metadata.getOrDefault(PROJECT_NAME_PREFIX, "orphanProject"); // get project name from metadata
rootProjectName = metadata.getOrDefault(ROOT_PROJECT_NAME_PREFIX, ""); // get root project name
projectParent = metadata.getOrDefault(PROJECT_PARENT_PREFIX, "null"); // get project parent name
String fileName = metadata.getOrDefault(FILE_NAME_PREFIX, "");


// To avoid a bug caused by an edge case where child and parent modules have the same name causing the loop for checking rich version to stuck
// in an infinite state, we are going to suffix the name of the project with the depth number
int s = fileName.indexOf("depth") + 5; // File name is like project__projectname__depth3_dependencyGraph.txt, we extract the number after depth
int e = fileName.indexOf("_dependencyGraph");
depthNumber = fileName.substring(s, e);
projectName = projectName+"_"+depthNumber;

addRelation();

// Example of dependency using rich version:
// --- com.graphql-java:graphql-java:{strictly [21.2, 21.3]; prefer 21.3; reject [20.6, 19.5, 18.2]} -> 21.3 direct depenendency, will be stored in rich versions, richVersionProject value will be current project
// +--- com.graphql-java:java-dataloader:3.2.1 transitive needs to be stored
// | \--- org.slf4j:slf4j-api:1.7.30 -> 2.0.4 transitive needs to be stored

if(gavPieces.size() == 3) {
String dependencyGroupName = gavPieces.get(0) + ":" + gavPieces.get(1);
if(level == 0 && checkRichVersionUse(cleanedOutput)) {
if(level == 0 && checkRichVersionUse(cleanedOutput)) { // we only track rich versions if they are declared in direct dependencies
storeDirectRichVersion(dependencyGroupName, gavPieces);
} else {
storeOrUpdateRichVersion(dependencyGroupName, gavPieces);
Expand All @@ -127,37 +145,53 @@ private List<String> parseGav(String line, Map<String, String> metadata) {
return gavPieces;
}

// store the dependency where rich version was declared and update the global tracking values
private void storeDirectRichVersion(String dependencyGroupName, List<String> gavPieces) {
gradleRichVersions.computeIfAbsent(projectName, value -> new HashMap<>()).putIfAbsent(dependencyGroupName, gavPieces.get(2));
richVersionProject = projectName;
richVersionDeclared = true;
}

private void storeOrUpdateRichVersion(String dependencyGroupName, List<String> gavPieces) {
// this condition is checking for rich version use for current direct dependency in one of the parent submodule of the current module and updates the current version
if (checkParentRichVersion(dependencyGroupName)) {
gavPieces.set(2, gradleRichVersions.get(richVersionProject).get(dependencyGroupName));
} else if(checkIfTransitiveRichVersion() && transitiveRichVersions.containsKey(richVersionProject) && transitiveRichVersions.get(richVersionProject).containsKey(dependencyGroupName)) {
// this is checking if we are parsing a transitive dependency and that transitive
// dependency has already been memoized for the use of rich version
gavPieces.set(2, transitiveRichVersions.get(richVersionProject).get(dependencyGroupName));
} else if (checkIfTransitiveRichVersion() && richVersionDeclared) {
// if while parsing the last direct dependency, we found the use of rich version, we store the version resolved for this transitive dependency
transitiveRichVersions.computeIfAbsent(richVersionProject, value -> new HashMap<>()).putIfAbsent(dependencyGroupName, gavPieces.get(2));
} else {
// no use of rich versions found
richVersionDeclared = false;
richVersionProject = null;
}
}

private void addRelation() {
// add parent-child relationships in the map to keep track of parent and child submodules
// project parent value will be of two types: one containing "project ':parentName'" or "root project ':rootProjectName'"
if (!projectParent.equals("null") && !projectParent.contains("root project")) {
// this will be the first case where we extract "parentName" from the value
String parentString = projectParent.substring(projectParent.lastIndexOf(":") + 1, projectParent.lastIndexOf("'"));
relationsMap.putIfAbsent(projectName, parentString);
int depth = Integer.parseInt(depthNumber);
String parentDepth = String.valueOf(depth-1);
// for submodules who have different parent than root project, we will suffix current depth - 1 to maintain uniformity, so if child is at depth2 than parent would be at depth1
relationsMap.putIfAbsent(projectName, parentString+"_"+parentDepth);
} else if (!projectParent.equals("null") && !projectName.equals(rootProjectName)) {
// this will be the second case where root project will be the parent
relationsMap.putIfAbsent(projectName, rootProjectName);
} else {
relationsMap.putIfAbsent(rootProjectName, null);
}
}

private boolean checkParentRichVersion(String dependencyGroupName) {
// this loop checks all the parent modules for the current submodule upto rootProject for the use of the rich version for the current dependency
// if the rich version is used return true and update the richVersionProject
// this loop will stop at the root project since its parent value would be null
String currentProject = projectName;
while (currentProject != null) {
if (gradleRichVersions.containsKey(currentProject) && gradleRichVersions.get(currentProject).containsKey(dependencyGroupName)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class GradleReportParser {
public static final String ROOT_PROJECT_VERSION_PREFIX = "rootProjectVersion:";
public static final String DETECT_META_DATA_HEADER = "DETECT META DATA START";
public static final String DETECT_META_DATA_FOOTER = "DETECT META DATA END";
private final Map<String, String> metadata = new HashMap<>();
private final Map<String, String> metadata = new HashMap<>(); // important metadata to pass to line parsers for rich versions
private final GradleReportConfigurationParser gradleReportConfigurationParser = new GradleReportConfigurationParser();

public Optional<GradleReport> parseReport(File reportFile) {
Expand All @@ -44,10 +44,14 @@ public Optional<GradleReport> parseReport(File reportFile) {

List<String> reportLines = reader.lines().collect(Collectors.toList());

// we parse the last few lines of the extraction file, as we have the metadata information at the end of the file
// such as project name, project parent etc. This information is helpful in getting the rich version information by storing parent information
// and then parse childs to see if the rich versions declared are used.
for(int i = reportLines.size()-1; i>=0; i--) {
if (reportLines.get(i).startsWith(DETECT_META_DATA_FOOTER)) {
processingMetaData = true;
} else if (reportLines.get(i).startsWith(DETECT_META_DATA_HEADER)) {
metadata.put("fileName:", reportFile.getName());
break;
} else if (processingMetaData) {
setGradleReportInfo(gradleReport, reportLines.get(i));
Expand Down Expand Up @@ -80,7 +84,7 @@ private void setGradleReportInfo(GradleReport gradleReport, String line) {
} else if (line.startsWith(PROJECT_GROUP_PREFIX)) {
gradleReport.setProjectGroup(line.substring(PROJECT_GROUP_PREFIX.length()).trim());
} else if (line.startsWith(PROJECT_NAME_PREFIX)) {
String projectName = line.substring(PROJECT_NAME_PREFIX.length()).trim();
String projectName = line.substring(PROJECT_NAME_PREFIX.length()).trim(); // get project name
gradleReport.setProjectName(projectName);
metadata.put(PROJECT_NAME_PREFIX, projectName);
} else if (line.startsWith(PROJECT_VERSION_PREFIX)) {
Expand All @@ -89,7 +93,7 @@ private void setGradleReportInfo(GradleReport gradleReport, String line) {
String rootProjectName = line.substring(ROOT_PROJECT_NAME_PREFIX.length()).trim();
metadata.put(ROOT_PROJECT_NAME_PREFIX, rootProjectName);
} else if (line.startsWith(PROJECT_PARENT_PREFIX)) {
String projectParent = line.substring(PROJECT_PARENT_PREFIX.length()).trim();
String projectParent = line.substring(PROJECT_PARENT_PREFIX.length()).trim(); // get current project's parent name
metadata.put(PROJECT_PARENT_PREFIX, projectParent);
}
}
Expand Down