Skip to content

Commit 98989e2

Browse files
committed
feat: add support for yarn package manager
1 parent f45d3b0 commit 98989e2

File tree

10 files changed

+261
-11
lines changed

10 files changed

+261
-11
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ vulnerability report.
4242
**Prerequisites**
4343

4444
- For Maven projects, analyzing a `pom.xml` file, you must have the `mvn` binary in your IDE's `PATH` environment.
45-
- For Node projects, analyzing a `package.json` file, you must have one of the corresponding package manager `npm` or `pnpm`, `node` binaries in your IDE's `PATH`
45+
- For Node projects, analyzing a `package.json` file, you must have one of the corresponding package manager `npm`, `pnpm` or `yarn`, `node` binaries in your IDE's `PATH`
4646
environment.
4747
- For Golang projects, analyzing a `go.mod` file, you must have the `go` binary in your IDE's `PATH` environment.
4848
- For Python projects, analyzing a `requirements.txt` file, you must have the `python3` and `pip3` binaries in your
@@ -86,9 +86,9 @@ according to your preferences.
8686
executables.
8787

8888
- **Node** :
89-
<br >Set the full path of the Node executable, which allows Exhort to locate and run one of the corresponding `npm` or `pnpm` command to resolve
89+
<br >Set the full path of the Node executable, which allows Exhort to locate and run one of the corresponding `npm`, `pnpm` or `yarn` command to resolve
9090
dependencies for Node projects.
91-
<br >Path of the directory containing the `node` executable is required by one of the corresponding `npm` or `pnpm` executable.
91+
<br >Path of the directory containing the `node` executable is required by one of the corresponding `npm`, `pnpm` or `yarn` executable.
9292
<br >If the paths are not provided, your IDE's `PATH` environment will be used to locate the executables.
9393

9494
- **Golang** :
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
12+
package org.jboss.tools.intellij.componentanalysis.yarn;
13+
14+
import com.intellij.json.psi.JsonArray;
15+
import com.intellij.json.psi.JsonObject;
16+
import com.intellij.json.psi.JsonProperty;
17+
import com.intellij.json.psi.JsonStringLiteral;
18+
import com.intellij.json.psi.JsonValue;
19+
import com.intellij.psi.PsiElement;
20+
import com.intellij.psi.PsiFile;
21+
import com.redhat.exhort.api.v4.DependencyReport;
22+
import org.jboss.tools.intellij.componentanalysis.CAAnnotator;
23+
import org.jboss.tools.intellij.componentanalysis.CAIntentionAction;
24+
import org.jboss.tools.intellij.componentanalysis.CAUpdateManifestIntentionAction;
25+
import org.jboss.tools.intellij.componentanalysis.Dependency;
26+
import org.jboss.tools.intellij.componentanalysis.VulnerabilitySource;
27+
28+
import java.util.Arrays;
29+
import java.util.Collections;
30+
import java.util.HashMap;
31+
import java.util.LinkedList;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.Set;
35+
import java.util.stream.Collectors;
36+
37+
public class YarnCAAnnotator extends CAAnnotator {
38+
39+
@Override
40+
protected String getInspectionShortName() {
41+
return YarnCAInspection.SHORT_NAME;
42+
}
43+
44+
@Override
45+
protected Map<Dependency, List<PsiElement>> getDependencies(PsiFile file) {
46+
if ("package.json".equals(file.getName())) {
47+
Set<String> ignored = Arrays.stream(file.getChildren())
48+
.filter(e -> e instanceof JsonObject)
49+
.flatMap(e -> Arrays.stream(e.getChildren()))
50+
.filter(e -> e instanceof JsonProperty && "exhortignore".equals(((JsonProperty) e).getName()))
51+
.flatMap(e -> Arrays.stream(e.getChildren()))
52+
.filter(e -> e instanceof JsonArray)
53+
.flatMap(e -> Arrays.stream(e.getChildren()))
54+
.filter(c -> c instanceof JsonStringLiteral)
55+
.map(c -> ((JsonStringLiteral) c).getValue())
56+
.collect(Collectors.toSet());
57+
58+
Map<Dependency, List<PsiElement>> resultMap = new HashMap<>();
59+
Arrays.stream(file.getChildren())
60+
.filter(e -> e instanceof JsonObject)
61+
.flatMap(e -> Arrays.stream(e.getChildren()))
62+
.filter(e -> e instanceof JsonProperty && "dependencies".equals(((JsonProperty) e).getName()))
63+
.flatMap(e -> Arrays.stream(e.getChildren()))
64+
.filter(e -> e instanceof JsonObject)
65+
.flatMap(e -> Arrays.stream(e.getChildren()))
66+
.filter(c -> c instanceof JsonProperty && !ignored.contains(((JsonProperty) c).getName()))
67+
.forEach(c -> {
68+
String name = ((JsonProperty) c).getName();
69+
int index = name.lastIndexOf("/");
70+
String namespace = null;
71+
if (index > 0) {
72+
namespace = name.substring(0, index);
73+
name = name.substring(index + 1);
74+
} else if (index == 0) {
75+
name = name.substring(index + 1);
76+
}
77+
JsonValue value = ((JsonProperty) c).getValue();
78+
String version = value instanceof JsonStringLiteral
79+
? ((JsonStringLiteral) value).getValue()
80+
: null;
81+
Dependency dp = new Dependency("yarn", namespace, name, version);
82+
resultMap.computeIfAbsent(dp, k -> new LinkedList<>()).add(c);
83+
});
84+
return resultMap;
85+
}
86+
return Collections.emptyMap();
87+
}
88+
89+
@Override
90+
protected CAIntentionAction createQuickFix(PsiElement element, VulnerabilitySource source, DependencyReport report) {
91+
return new YarnCAIntentionAction(element, source, report);
92+
}
93+
94+
@Override
95+
protected CAUpdateManifestIntentionAction patchManifest(PsiElement element, DependencyReport report) {
96+
return null;
97+
}
98+
99+
@Override
100+
protected boolean isQuickFixApplicable(PsiElement element) {
101+
return element instanceof JsonProperty && ((JsonProperty) element).getValue() instanceof JsonStringLiteral;
102+
}
103+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
12+
package org.jboss.tools.intellij.componentanalysis.yarn;
13+
14+
import com.intellij.codeHighlighting.HighlightDisplayLevel;
15+
import com.intellij.codeInspection.LocalInspectionTool;
16+
import org.jetbrains.annotations.Nls;
17+
import org.jetbrains.annotations.NonNls;
18+
import org.jetbrains.annotations.NotNull;
19+
20+
public class YarnCAInspection extends LocalInspectionTool {
21+
22+
@NonNls
23+
public static final String SHORT_NAME = "YarnCAInspection";
24+
25+
@Override
26+
@NotNull
27+
public String getGroupDisplayName() {
28+
return "Imports and dependencies";
29+
}
30+
31+
@Override
32+
public @Nls(capitalization = Nls.Capitalization.Sentence) String @NotNull [] getGroupPath() {
33+
return new String[]{"JavaScript and TypeScript"};
34+
}
35+
36+
@Override
37+
@NotNull
38+
public String getShortName() {
39+
return SHORT_NAME;
40+
}
41+
42+
@Override
43+
@NotNull
44+
public HighlightDisplayLevel getDefaultLevel() {
45+
return HighlightDisplayLevel.ERROR;
46+
}
47+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
12+
package org.jboss.tools.intellij.componentanalysis.yarn;
13+
14+
import com.intellij.codeInsight.intention.FileModifier;
15+
import com.intellij.json.psi.JsonElementGenerator;
16+
import com.intellij.json.psi.JsonProperty;
17+
import com.intellij.json.psi.JsonStringLiteral;
18+
import com.intellij.openapi.editor.Editor;
19+
import com.intellij.openapi.project.Project;
20+
import com.intellij.psi.PsiElement;
21+
import com.intellij.psi.PsiFile;
22+
import com.redhat.exhort.api.v4.DependencyReport;
23+
import org.jboss.tools.intellij.componentanalysis.CAIntentionAction;
24+
import org.jboss.tools.intellij.componentanalysis.VulnerabilitySource;
25+
import org.jetbrains.annotations.NotNull;
26+
import org.jetbrains.annotations.Nullable;
27+
28+
public final class YarnCAIntentionAction extends CAIntentionAction {
29+
YarnCAIntentionAction(PsiElement element, VulnerabilitySource source, DependencyReport report) {
30+
super(element, source, report);
31+
}
32+
33+
@Override
34+
protected void updateVersion(@NotNull Project project, Editor editor, PsiFile file, String version) {
35+
JsonStringLiteral value = (JsonStringLiteral) ((JsonProperty) element).getValue();
36+
if (value != null) {
37+
JsonStringLiteral newValue = new JsonElementGenerator(project).createStringLiteral(version);
38+
value.replace(newValue);
39+
}
40+
}
41+
42+
@Override
43+
protected @Nullable FileModifier createCAIntentionActionInCopy(PsiElement element) {
44+
return new YarnCAIntentionAction(element, this.source, this.report);
45+
}
46+
47+
@Override
48+
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
49+
return file != null && "package.json".equals(file.getName());
50+
}
51+
}

src/main/java/org/jboss/tools/intellij/exhort/ApiService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ private void setRequestProperties(final String manifestName) {
149149
} else {
150150
System.clearProperty("EXHORT_NPM_PATH");
151151
}
152+
if (settings.yarnPath != null && !settings.yarnPath.isBlank()) {
153+
System.setProperty("EXHORT_YARN_PATH", settings.yarnPath);
154+
} else {
155+
System.clearProperty("EXHORT_YARN_PATH");
156+
}
152157
if (settings.nodePath != null && !settings.nodePath.isBlank()) {
153158
System.setProperty("NODE_HOME", settings.nodePath);
154159
} else {

src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
1717
import com.intellij.ui.components.JBCheckBox;
1818
import com.intellij.ui.components.JBLabel;
19-
import com.intellij.ui.components.JBRadioButton;
2019
import com.intellij.ui.components.JBTextField;
2120
import com.intellij.util.ui.FormBuilder;
2221
import org.jetbrains.annotations.NotNull;
2322

24-
import javax.swing.*;
25-
import java.awt.*;
23+
import javax.swing.JComponent;
24+
import javax.swing.JPanel;
2625

2726
public class ApiSettingsComponent {
2827

@@ -34,6 +33,8 @@ public class ApiSettingsComponent {
3433
+ "<br>Specifies absolute path of <b>npm</b> executable.</html>";
3534
private final static String pnpmPathLabel = "<html>Pnpm > Executable: <b>Path</b>"
3635
+ "<br>Specifies absolute path of <b>pnpm</b> executable.</html>";
36+
private final static String yarnPathLabel = "<html>Yarn > Executable: <b>Path</b>"
37+
+ "<br>Specifies absolute path of <b>yarn</b> executable.</html>";
3738
private final static String nodePathLabel = "<html>Node > Directory: <b>Path</b>"
3839
+ "<br>Specifies absolute path of the <i>directory</i> containing <b>node</b> executable.</html>";
3940
private final static String goPathLabel = "<html>Go > Executable: <b>Path</b>"
@@ -75,6 +76,7 @@ public class ApiSettingsComponent {
7576
private final TextFieldWithBrowseButton javaPathText;
7677
private final TextFieldWithBrowseButton npmPathText;
7778
private final TextFieldWithBrowseButton pnpmPathText;
79+
private final TextFieldWithBrowseButton yarnPathText;
7880
private final TextFieldWithBrowseButton nodePathText;
7981
private final TextFieldWithBrowseButton goPathText;
8082
private final JBCheckBox goMatchManifestVersionsCheck;
@@ -133,6 +135,15 @@ public ApiSettingsComponent() {
133135
TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT
134136
);
135137

138+
yarnPathText = new TextFieldWithBrowseButton();
139+
yarnPathText.addBrowseFolderListener(
140+
null,
141+
null,
142+
null,
143+
FileChooserDescriptorFactory.createSingleFileDescriptor(),
144+
TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT
145+
);
146+
136147
nodePathText = new TextFieldWithBrowseButton();
137148
nodePathText.addBrowseFolderListener(
138149
null,
@@ -253,6 +264,8 @@ public ApiSettingsComponent() {
253264
.addVerticalGap(10)
254265
.addLabeledComponent(new JBLabel(pnpmPathLabel), pnpmPathText, 1, true)
255266
.addVerticalGap(10)
267+
.addLabeledComponent(new JBLabel(yarnPathLabel), yarnPathText, 1, true)
268+
.addVerticalGap(10)
256269
.addLabeledComponent(new JBLabel(nodePathLabel), nodePathText, 1, true)
257270
.addSeparator(10)
258271
.addVerticalGap(10)
@@ -339,6 +352,14 @@ public void setPnpmPathText(@NotNull String text) {
339352
pnpmPathText.setText(text);
340353
}
341354

355+
public String getYarnPathText() {
356+
return yarnPathText.getText();
357+
}
358+
359+
public void setYarnPathText(@NotNull String text) {
360+
yarnPathText.setText(text);
361+
}
362+
342363
@NotNull
343364
public String getNodePathText() {
344365
return nodePathText.getText();

src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import com.intellij.openapi.util.NlsContexts;
1515
import org.jetbrains.annotations.Nullable;
1616

17-
import javax.swing.*;
17+
import javax.swing.JComponent;
1818

1919
public class ApiSettingsConfigurable implements com.intellij.openapi.options.Configurable {
2020

@@ -43,6 +43,7 @@ public boolean isModified() {
4343
modified |= !settingsComponent.getJavaPathText().equals(settings.javaPath);
4444
modified |= !settingsComponent.getNpmPathText().equals(settings.npmPath);
4545
modified |= !settingsComponent.getPnpmPathText().equals(settings.pnpmPath);
46+
modified |= !settingsComponent.getYarnPathText().equals(settings.yarnPath);
4647
modified |= !settingsComponent.getNodePathText().equals(settings.nodePath);
4748
modified |= !settingsComponent.getGoPathText().equals(settings.goPath);
4849
modified |= settingsComponent.getGoMatchManifestVersionsCheck() != settings.goMatchManifestVersions;
@@ -70,6 +71,7 @@ public void apply() {
7071
settings.javaPath = settingsComponent.getJavaPathText();
7172
settings.npmPath = settingsComponent.getNpmPathText();
7273
settings.pnpmPath = settingsComponent.getPnpmPathText();
74+
settings.yarnPath = settingsComponent.getYarnPathText();
7375
settings.nodePath = settingsComponent.getNodePathText();
7476
settings.goPath = settingsComponent.getGoPathText();
7577
settings.goMatchManifestVersions = settingsComponent.getGoMatchManifestVersionsCheck();
@@ -96,6 +98,7 @@ public void reset() {
9698
settingsComponent.setJavaPathText(settings.javaPath != null ? settings.javaPath : "");
9799
settingsComponent.setNpmPathText(settings.npmPath != null ? settings.npmPath : "");
98100
settingsComponent.setPnpmPathText(settings.pnpmPath != null ? settings.pnpmPath : "");
101+
settingsComponent.setYarnPathText(settings.yarnPath != null ? settings.yarnPath : "");
99102
settingsComponent.setNodePathText(settings.nodePath != null ? settings.nodePath : "");
100103
settingsComponent.setGoPathText(settings.goPath != null ? settings.goPath : "");
101104
settingsComponent.setGoMatchManifestVersionsCheck(settings.goMatchManifestVersions);

src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public final class ApiSettingsState implements PersistentStateComponent<ApiSetti
4040

4141
public String npmPath;
4242
public String pnpmPath;
43+
public String yarnPath;
4344
public String nodePath;
4445

4546
public String goPath;

src/main/resources/META-INF/plugin.xml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<p>
2222
<b>IMPORTANT:</b>
2323
<br>Currently, Dependency Analytics only supports projects that use Maven (<code>mvn</code>), and Node
24-
(<code>npm</code> or <code>pnpm</code>), Golang (<code>go mod</code>) and Python (<code>pip</code>) ecosystems, and base images in
24+
(<code>npm</code>, <code>pnpm</code> or <code>yarn</code>), Golang (<code>go mod</code>) and Python (<code>pip</code>) ecosystems, and base images in
2525
<code>Dockerfile</code>.
2626
<br>In future releases, Red Hat plans to support other programming languages.
2727
<p>
@@ -33,7 +33,7 @@
3333
<li>For Maven projects, analyzing a <code>pom.xml</code> file, you must have the <code>mvn</code> binary in your
3434
IDE's <code>PATH</code> environment.
3535
</li>
36-
<li>For Node projects, analyzing a <code>package.json</code> file, you must have one of the corresponding package manager <code>npm</code> or <code>pnpm</code> and
36+
<li>For Node projects, analyzing a <code>package.json</code> file, you must have one of the corresponding package manager <code>npm</code>, <code>pnpm</code> or <code>yarn</code> and
3737
<code>node</code> binaries in your IDE's <code>PATH</code> environment.
3838
</li>
3939
<li>For Golang projects, analyzing a <code>go.mod</code> file, you must have the <code>go</code> binary in your
@@ -99,9 +99,9 @@
9999
</li>
100100
<li>
101101
<b>Node</b>:
102-
<br>Set the full path of the Node executable, which allows Exhort to locate and execute one of the corresponding <code>npm</code> or <code>pnpm</code> command
102+
<br>Set the full path of the Node executable, which allows Exhort to locate and execute one of the corresponding <code>npm</code>, <code>pnpm</code> or <code>yarn</code> command
103103
to resolve dependencies for Node projects.
104-
<br>Path of the directory containing the <code>node</code> executable is required by one of the corresponding package manager <code>npm</code> or <code>pnpm</code>
104+
<br>Path of the directory containing the <code>node</code> executable is required by one of the corresponding package manager <code>npm</code>, <code>pnpm</code> or <code>yarn</code>
105105
executable.
106106
<br>If the paths are not provided, your IDE's <code>PATH</code> environment will be used to locate the
107107
executables.
@@ -459,6 +459,13 @@
459459
<externalAnnotator language="JSON"
460460
implementationClass="org.jboss.tools.intellij.componentanalysis.pnpm.PnpmCAAnnotator"/>
461461

462+
<localInspection language="JSON" shortName="YarnCAInspection"
463+
displayName="Red Hat Dependency Analytics component analysis"
464+
groupPath="JavaScript and TypeScript" groupName="Imports and dependencies"
465+
enabledByDefault="true" level="ERROR"
466+
implementationClass="org.jboss.tools.intellij.componentanalysis.yarn.YarnCAInspection"/>
467+
<externalAnnotator language="JSON"
468+
implementationClass="org.jboss.tools.intellij.componentanalysis.yarn.YarnCAAnnotator"/>
462469
<fileType name="rhda-requirements"
463470
language="rhda-requirements"
464471
fileNames="requirements.txt"

0 commit comments

Comments
 (0)