Skip to content

Commit f45d3b0

Browse files
authored
feat: Add support for pnpm package manager (#180)
* feat: add support for pnpm package manager * chore: resolving dependencies from local maven repository
1 parent af28585 commit f45d3b0

File tree

11 files changed

+261
-7
lines changed

11 files changed

+261
-7
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 the `npm` and `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` or `pnpm`, `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 the `npm` 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` or `pnpm` command to resolve
9090
dependencies for Node projects.
91-
<br >Path of the directory containing the `node` executable is required by the `npm` executable.
91+
<br >Path of the directory containing the `node` executable is required by one of the corresponding `npm` or `pnpm` executable.
9292
<br >If the paths are not provided, your IDE's `PATH` environment will be used to locate the executables.
9393

9494
- **Golang** :

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ repositories {
2525
password = exhortRepoToken
2626
}
2727
}
28+
mavenLocal()
2829
}
2930

3031
// Include the generated files in the source set
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.pnpm;
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 PnpmCAAnnotator extends CAAnnotator {
38+
39+
@Override
40+
protected String getInspectionShortName() {
41+
return PnpmCAInspection.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("npm", 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 PnpmCAIntentionAction(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.pnpm;
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 PnpmCAInspection extends LocalInspectionTool {
21+
22+
@NonNls
23+
public static final String SHORT_NAME = "PnpmCAInspection";
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.pnpm;
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 PnpmCAIntentionAction extends CAIntentionAction {
29+
PnpmCAIntentionAction(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 PnpmCAIntentionAction(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
@@ -154,6 +154,11 @@ private void setRequestProperties(final String manifestName) {
154154
} else {
155155
System.clearProperty("NODE_HOME");
156156
}
157+
if (settings.pnpmPath != null && !settings.pnpmPath.isBlank()) {
158+
System.setProperty("EXHORT_PNPM_PATH", settings.pnpmPath);
159+
} else {
160+
System.clearProperty("EXHORT_PNPM_PATH");
161+
}
157162
if (settings.goPath != null && !settings.goPath.isBlank()) {
158163
System.setProperty("EXHORT_GO_PATH", settings.goPath);
159164
} else {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public class ApiSettingsComponent {
3232
+ "<br>Specifies absolute path of Java installation directory.</html>";
3333
private final static String npmPathLabel = "<html>Npm > Executable: <b>Path</b>"
3434
+ "<br>Specifies absolute path of <b>npm</b> executable.</html>";
35+
private final static String pnpmPathLabel = "<html>Pnpm > Executable: <b>Path</b>"
36+
+ "<br>Specifies absolute path of <b>pnpm</b> executable.</html>";
3537
private final static String nodePathLabel = "<html>Node > Directory: <b>Path</b>"
3638
+ "<br>Specifies absolute path of the <i>directory</i> containing <b>node</b> executable.</html>";
3739
private final static String goPathLabel = "<html>Go > Executable: <b>Path</b>"
@@ -72,6 +74,7 @@ public class ApiSettingsComponent {
7274
private final TextFieldWithBrowseButton mvnPathText;
7375
private final TextFieldWithBrowseButton javaPathText;
7476
private final TextFieldWithBrowseButton npmPathText;
77+
private final TextFieldWithBrowseButton pnpmPathText;
7578
private final TextFieldWithBrowseButton nodePathText;
7679
private final TextFieldWithBrowseButton goPathText;
7780
private final JBCheckBox goMatchManifestVersionsCheck;
@@ -121,6 +124,15 @@ public ApiSettingsComponent() {
121124
TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT
122125
);
123126

127+
pnpmPathText = new TextFieldWithBrowseButton();
128+
pnpmPathText.addBrowseFolderListener(
129+
null,
130+
null,
131+
null,
132+
FileChooserDescriptorFactory.createSingleFileDescriptor(),
133+
TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT
134+
);
135+
124136
nodePathText = new TextFieldWithBrowseButton();
125137
nodePathText.addBrowseFolderListener(
126138
null,
@@ -239,6 +251,8 @@ public ApiSettingsComponent() {
239251
.addVerticalGap(10)
240252
.addLabeledComponent(new JBLabel(npmPathLabel), npmPathText, 1, true)
241253
.addVerticalGap(10)
254+
.addLabeledComponent(new JBLabel(pnpmPathLabel), pnpmPathText, 1, true)
255+
.addVerticalGap(10)
242256
.addLabeledComponent(new JBLabel(nodePathLabel), nodePathText, 1, true)
243257
.addSeparator(10)
244258
.addVerticalGap(10)
@@ -316,6 +330,15 @@ public void setNpmPathText(@NotNull String text) {
316330
npmPathText.setText(text);
317331
}
318332

333+
@NotNull
334+
public String getPnpmPathText() {
335+
return pnpmPathText.getText();
336+
}
337+
338+
public void setPnpmPathText(@NotNull String text) {
339+
pnpmPathText.setText(text);
340+
}
341+
319342
@NotNull
320343
public String getNodePathText() {
321344
return nodePathText.getText();

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public boolean isModified() {
4242
boolean modified = !settingsComponent.getMvnPathText().equals(settings.mvnPath);
4343
modified |= !settingsComponent.getJavaPathText().equals(settings.javaPath);
4444
modified |= !settingsComponent.getNpmPathText().equals(settings.npmPath);
45+
modified |= !settingsComponent.getPnpmPathText().equals(settings.pnpmPath);
4546
modified |= !settingsComponent.getNodePathText().equals(settings.nodePath);
4647
modified |= !settingsComponent.getGoPathText().equals(settings.goPath);
4748
modified |= settingsComponent.getGoMatchManifestVersionsCheck() != settings.goMatchManifestVersions;
@@ -68,6 +69,7 @@ public void apply() {
6869
settings.mvnPath = settingsComponent.getMvnPathText();
6970
settings.javaPath = settingsComponent.getJavaPathText();
7071
settings.npmPath = settingsComponent.getNpmPathText();
72+
settings.pnpmPath = settingsComponent.getPnpmPathText();
7173
settings.nodePath = settingsComponent.getNodePathText();
7274
settings.goPath = settingsComponent.getGoPathText();
7375
settings.goMatchManifestVersions = settingsComponent.getGoMatchManifestVersionsCheck();
@@ -93,6 +95,7 @@ public void reset() {
9395
settingsComponent.setMvnPathText(settings.mvnPath != null ? settings.mvnPath : "");
9496
settingsComponent.setJavaPathText(settings.javaPath != null ? settings.javaPath : "");
9597
settingsComponent.setNpmPathText(settings.npmPath != null ? settings.npmPath : "");
98+
settingsComponent.setPnpmPathText(settings.pnpmPath != null ? settings.pnpmPath : "");
9699
settingsComponent.setNodePathText(settings.nodePath != null ? settings.nodePath : "");
97100
settingsComponent.setGoPathText(settings.goPath != null ? settings.goPath : "");
98101
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
@@ -39,6 +39,7 @@ public final class ApiSettingsState implements PersistentStateComponent<ApiSetti
3939
public String javaPath;
4040

4141
public String npmPath;
42+
public String pnpmPath;
4243
public String nodePath;
4344

4445
public String goPath;

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

Lines changed: 12 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>), Golang (<code>go mod</code>) and Python (<code>pip</code>) ecosystems, and base images in
24+
(<code>npm</code> or <code>pnpm</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 the <code>npm</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> or <code>pnpm</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 <code>npm</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> or <code>pnpm</code> command
103103
to resolve dependencies for Node projects.
104-
<br>Path of the directory containing the <code>node</code> executable is required by the <code>npm</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> or <code>pnpm</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.
@@ -451,6 +451,14 @@
451451
<externalAnnotator language="JSON"
452452
implementationClass="org.jboss.tools.intellij.componentanalysis.npm.NpmCAAnnotator"/>
453453

454+
<localInspection language="JSON" shortName="PnpmCAInspection"
455+
displayName="Red Hat Dependency Analytics component analysis"
456+
groupPath="JavaScript and TypeScript" groupName="Imports and dependencies"
457+
enabledByDefault="true" level="ERROR"
458+
implementationClass="org.jboss.tools.intellij.componentanalysis.pnpm.PnpmCAInspection"/>
459+
<externalAnnotator language="JSON"
460+
implementationClass="org.jboss.tools.intellij.componentanalysis.pnpm.PnpmCAAnnotator"/>
461+
454462
<fileType name="rhda-requirements"
455463
language="rhda-requirements"
456464
fileNames="requirements.txt"

0 commit comments

Comments
 (0)