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 malformed External ID generation, obtuse warning message and unmatched version resolution in Yarn detector #1290

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

gopannabd
Copy link
Contributor

@gopannabd gopannabd commented Nov 5, 2024

Description

The root cause for the issue is JSON: why are forward slashes escaped?

This issue is surfaced by the dependency on Apache Commons : ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE)

This returns a string with escaped forward slash in the JSON format. This is used in Stringable class of integration-common library which in turn is extended by ExternalID class in integration-bdio library.

When external ID is generated from name and version of entries in yarn.lock file, we use a factory method that generates a String. This same string is recorded and referred to again for potential matches as we build the dependency graph. When we do, we do not match because the String with escaped forward slash does not return the same component name for comparison.

To fix this, we have addressed it in the Yarn Detect code to remove the escape character for forward slash whenever we generate the external ID as String for comparison.

While the malformed ID is fixed, the warning message itself was not intuitive and is confusing. It is now reworded for better clarity to mean that the entries in the yarn.lock file that have not so far been resolved through package.json files, could not be resolved with any standard NPM packages either.

Additionally, the default workflow now provides advanced version resolution for unmatched components, if any, using the root yarn lock file. This addresses the requirement of IDETECT-4342 indirectly by resolving the warning messages of unresolved versions when default Yarn detector is used. Yarn 4 is support is unaffected.

Github Issues

IDETECT-4447, IDETECT-4314, IDETECT-4342

@gopannabd gopannabd self-assigned this Nov 5, 2024
@gopannabd gopannabd changed the title Idetect 4447 test Fix malformed External ID generation, obtuse warning message and unmatched version resolution in Yarn detector Nov 7, 2024
@@ -46,7 +46,7 @@ public Extraction extract(File projectDir, File yarnLockFile, File rootPackageJs
YarnLock yarnLock = readYarnLock(yarnLockFile);

YarnResult yarnResult;
if (yarnLockOptions.getMonorepoMode()) {
if (yarnLockOptions.getYarnIgnoreAllWorkspacesMode()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for consistency.

@@ -8,17 +8,17 @@ public class YarnLockOptions {
private final EnumListFilter<YarnDependencyType> yarnDependencyTypeFilter;
private final List<String> excludedWorkspaceNamePatterns;
private final List<String> includedWorkspaceNamePatterns;
private final Boolean monorepoMode;
private final Boolean yarnIgnoreAllWorkspacesMode;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for consistency.


public YarnLockOptions(
EnumListFilter<YarnDependencyType> yarnDependencyTypeFilter,
List<String> excludedWorkspaceNamePatterns,
List<String> includedWorkspaceNamePatterns,
Boolean monorepoMode) {
Boolean yarnIgnoreAllWorkspacesMode) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for consistency.

this.yarnDependencyTypeFilter = yarnDependencyTypeFilter;
this.excludedWorkspaceNamePatterns = excludedWorkspaceNamePatterns;
this.includedWorkspaceNamePatterns = includedWorkspaceNamePatterns;
this.monorepoMode = monorepoMode;
this.yarnIgnoreAllWorkspacesMode = yarnIgnoreAllWorkspacesMode;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for consistency.

@@ -33,7 +33,7 @@ public List<String> getIncludedWorkspaceNamePatterns() {
return includedWorkspaceNamePatterns;
}

public Boolean getMonorepoMode() {
return monorepoMode;
public Boolean getYarnIgnoreAllWorkspacesMode() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for consistency.

@@ -39,7 +37,7 @@ public class YarnTransformer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public static final String STRING_ID_NAME_VERSION_SEPARATOR = "@";
private final ExternalIdFactory externalIdFactory;
private final Set<LazyId> unMatchedDependencies = new HashSet<>();
private final Map<LazyId, Optional<NameVersion>> unMatchedDependencies = new HashMap<>();
Copy link
Contributor Author

@gopannabd gopannabd Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used a map to retain NameVersion corresponding to LazyID to avoid lossy reconversion later.

@@ -50,10 +48,10 @@ public YarnTransformer(ExternalIdFactory externalIdFactory, EnumListFilter<YarnD
public List<CodeLocation> generateCodeLocations(YarnLockResult yarnLockResult, List<NameVersion> externalDependencies)
throws MissingExternalIdException {
List<CodeLocation> codeLocations = new LinkedList<>();
logger.debug("Adding root dependencies for project: {}:{}", yarnLockResult.getRootPackageJson().getNameString(), yarnLockResult.getRootPackageJson().getVersionString());
logger.debug("Adding root dependencies for project: {}:{}, externalDeps: {}", yarnLockResult.getRootPackageJson().getNameString(), yarnLockResult.getRootPackageJson().getVersionString(), externalDependencies.size());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional data in debug logger on external dependencies available.

LazyBuilderMissingExternalIdHandler lazyBuilderHandler = getLazyBuilderHandler(externalDependencies);
ExternalIdDependencyGraphBuilder rootGraphBuilder = new ExternalIdDependencyGraphBuilder();
addRootDependenciesForProjectOrWorkspace(yarnLockResult, yarnLockResult.getRootPackageJson(), rootGraphBuilder);
addRootDependenciesForProject(yarnLockResult, yarnLockResult.getRootPackageJson(), rootGraphBuilder);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method addRootDependenciesForProject(...) belongs to 'ignore all workspaces' workflow.

@@ -83,29 +81,33 @@ private DependencyGraph buildGraphForProjectOrWorkspace(
List<NameVersion> externalDependencies
) throws MissingExternalIdException {
LazyExternalIdDependencyGraphBuilder graphBuilder = new LazyExternalIdDependencyGraphBuilder();
addRootNodesToGraph(graphBuilder, projectOrWorkspacePackageJson, yarnLockResult.getWorkspaceData());
addRootDependenciesForProjectOrWorkspace(graphBuilder, projectOrWorkspacePackageJson, yarnLockResult.getWorkspaceData());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored code by removing need for addRootNodesToGraph(...)

for (YarnLockDependency dependency : entry.getDependencies()) {
if (!isWorkspace(yarnLockResult.getWorkspaceData(), dependency)) {
LazyId stringDependencyId = generateComponentDependencyId(dependency.getName(), dependency.getVersion());

if (yarnDependencyTypeFilter.shouldInclude(YarnDependencyType.NON_PRODUCTION) || !dependency.isOptional()) {
yarnDependencies.put(stringDependencyId, new NameVersion(dependency.getName(), entry.getVersion()));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retained NameVersion pairs for LazyId.

unMatchedDependencies.add(dependencyId);
Optional<NameVersion> nameVersionOptional;
if (!unMatchedDependencies.containsKey(dependencyId)) {
String dependencyIdClean = dependencyId.toString().replace("\\/","/");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed escape character from string of dependency ID before use to reconvert to NameVersion.

if (!unMatchedDependencies.containsKey(dependencyId)) {
String dependencyIdClean = dependencyId.toString().replace("\\/","/");
nameVersionOptional = getNameVersion(dependencyIdClean);
logger.warn("No standard NPM package identification details are available for '{}' from the yarn.lock file", dependencyIdClean);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved warning message about lack of standard NPM details to resolve remaining unmatched components.

logger.warn("No standard NPM package identification details are available for '{}' from the yarn.lock file", dependencyIdClean);
unMatchedDependencies.put(dependencyId, nameVersionOptional);
} else {
nameVersionOptional = unMatchedDependencies.get(dependencyId);
Copy link
Contributor Author

@gopannabd gopannabd Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If matched, then use the retained NameVersion from map.

}
lazilyGeneratedExternalId = generateComponentExternalId(dependencyId);
return lazilyGeneratedExternalId;
return generateComponentExternalId(nameVersionOptional.get().getName(), nameVersionOptional.get().getVersion());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExternalID generated from NameVersion avoids escape character issue.

Optional<YarnWorkspace> dependencyWorkspace = workspaceData.lookup(rootDependency.getKey(), rootDependency.getValue());
if (dependencyWorkspace.isPresent()) {
logger.trace("Omitting dependency {}/{} because it's a workspace", rootDependency.getKey(), rootDependency.getValue());
private LazyBuilderMissingExternalIdHandler getLazyBuilderHandler(List<NameVersion> externalDependencies, Map<LazyId, NameVersion> yarnLockDependencies) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate method for default Yarn detector.

@@ -210,42 +212,68 @@ private LazyBuilderMissingExternalIdHandler getLazyBuilderHandler(List<NameVersi
if (externalId.isPresent()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate method for ignore-all-workspaces Yarn detector workflow.

};
}

private Optional<NameVersion> getNameVersion(String dependencyIdString) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converts given dependency ID string to NameVersion pair object.

}

private void addRootNodesToGraph(LazyExternalIdDependencyGraphBuilder graphBuilder, NullSafePackageJson projectOrWorkspacePackageJson, YarnWorkspaces workspaceData) {
populateGraphWithRootDependencies(graphBuilder, projectOrWorkspacePackageJson, workspaceData);
private void addRootDependenciesForProject(YarnLockResult yarnLockResult, NullSafePackageJson projectPackageJson, LazyExternalIdDependencyGraphBuilder graphBuilder) throws MissingExternalIdException {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method refactored.

}

private void populateGraphWithRootDependencies(LazyExternalIdDependencyGraphBuilder graphBuilder, NullSafePackageJson rootPackageJson, YarnWorkspaces workspaceData) {
private void addRootDependenciesForProjectOrWorkspace(LazyExternalIdDependencyGraphBuilder graphBuilder, NullSafePackageJson rootPackageJson, YarnWorkspaces workspaceData) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method refactored.

@@ -266,14 +294,10 @@ private void addRootDependenciesToGraph(LazyExternalIdDependencyGraphBuilder gra
}

private LazyId generateComponentDependencyId(String name, String version) {
return LazyId.fromString(name + STRING_ID_NAME_VERSION_SEPARATOR + version);
return LazyId.fromNameAndVersion(name, version);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch to NameVersion method to generate external ID.

@@ -1715,7 +1715,7 @@ private DetectProperties() {
.setGroups(DetectGroup.PATHS, DetectGroup.GLOBAL)
.build();

public static final BooleanProperty DETECT_YARN_MONOREPO_MODE =
public static final BooleanProperty DETECT_YARN_IGNORE_ALL_WORKSPACES_MODE =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for consistency.

@@ -284,12 +284,12 @@ public SbtDetectableOptions createSbtDetectableOptions() {

public YarnLockOptions createYarnLockOptions() {
Set<YarnDependencyType> excludedDependencyTypes = detectConfiguration.getValue(DetectProperties.DETECT_YARN_DEPENDENCY_TYPES_EXCLUDED).representedValueSet();
Boolean monorepoMode = detectConfiguration.getValue(DetectProperties.DETECT_YARN_MONOREPO_MODE);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for consistency.

EnumListFilter<YarnDependencyType> yarnDependencyTypeFilter = EnumListFilter.fromExcluded(excludedDependencyTypes);

List<String> excludedWorkspaces = detectConfiguration.getValue(DetectProperties.DETECT_YARN_EXCLUDED_WORKSPACES);
List<String> includedWorkspaces = detectConfiguration.getValue(DetectProperties.DETECT_YARN_INCLUDED_WORKSPACES);
return new YarnLockOptions(yarnDependencyTypeFilter, excludedWorkspaces, includedWorkspaces, monorepoMode);
return new YarnLockOptions(yarnDependencyTypeFilter, excludedWorkspaces, includedWorkspaces, yarnIgnoreAllWorkspacesMode);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for consistency.

@gopannabd
Copy link
Contributor Author

@andrian-sevastyanov @devmehtabd Please review the last committed changes.

@Override
public int compareTo(Version o) {
if (this.major == o.major
&& this.minor == o.minor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we missing the patch comparison here or is this for some reason purposeful?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants