Skip to content

Commit b58cda4

Browse files
authored
Add Problem Reporting API and provide Access Transformer Errors in a Machine Readable Format (#50)
1 parent aed6ed6 commit b58cda4

File tree

25 files changed

+322
-29
lines changed

25 files changed

+322
-29
lines changed

accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/AccessTransformersTransformer.java

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,30 @@
88
import net.neoforged.jst.api.Replacements;
99
import net.neoforged.jst.api.SourceTransformer;
1010
import net.neoforged.jst.api.TransformContext;
11+
import net.neoforged.problems.Problem;
12+
import net.neoforged.problems.ProblemGroup;
13+
import net.neoforged.problems.ProblemId;
14+
import net.neoforged.problems.ProblemLocation;
15+
import net.neoforged.problems.ProblemReporter;
16+
import net.neoforged.problems.ProblemSeverity;
1117
import picocli.CommandLine;
1218

13-
import java.io.IOException;
14-
import java.io.UncheckedIOException;
1519
import java.nio.file.Path;
20+
import java.nio.file.Paths;
1621
import java.util.List;
1722
import java.util.Map;
1823
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.regex.Pattern;
1925

2026
public class AccessTransformersTransformer implements SourceTransformer {
2127

28+
private static final ProblemGroup PROBLEM_GROUP = ProblemGroup.create("access-transformer", "Access Transformers");
29+
static final ProblemId INVALID_AT = ProblemId.create("invalid-at", "Invalid", PROBLEM_GROUP);
30+
private static final ProblemId MISSING_TARGET = ProblemId.create("missing-target", "Missing Target", PROBLEM_GROUP);
31+
32+
private static final Pattern LINE_PATTERN = Pattern.compile("\\bline\\s+(\\d+)");
33+
private static final Pattern ORIGIN_PATTERN = Pattern.compile("(.*):(\\d+)$");
34+
2235
@CommandLine.Option(names = "--access-transformer", required = true)
2336
public List<Path> atFiles;
2437

@@ -28,19 +41,36 @@ public class AccessTransformersTransformer implements SourceTransformer {
2841
private AccessTransformerFiles ats;
2942
private Map<Target, Transformation> pendingATs;
3043
private Logger logger;
44+
private ProblemReporter problemReporter;
3145
private volatile boolean errored;
3246

3347
@Override
3448
public void beforeRun(TransformContext context) {
3549
ats = new AccessTransformerFiles();
3650
logger = context.logger();
51+
problemReporter = context.problemReporter();
3752

3853
for (Path path : atFiles) {
3954
try {
4055
ats.loadFromPath(path);
41-
} catch (IOException ex) {
42-
logger.error("Failed to parse access transformer file %s: %s", path, ex.getMessage());
43-
throw new UncheckedIOException(ex);
56+
} catch (Exception e) {
57+
logger.error("Failed to parse access transformer file %s: %s", path, e.getMessage());
58+
59+
if (e.getMessage() != null) {
60+
var m = LINE_PATTERN.matcher(e.getMessage());
61+
if (m.matches()) {
62+
// The AT parser internally uses 0-based line numbering, but the problem reporter uses 1-based
63+
int line = 1 + Integer.parseUnsignedInt(m.group(1));
64+
problemReporter.report(INVALID_AT, ProblemSeverity.ERROR, ProblemLocation.ofLocationInFile(path, line), e.getMessage());
65+
} else {
66+
problemReporter.report(INVALID_AT, ProblemSeverity.ERROR, ProblemLocation.ofFile(path), e.getMessage());
67+
}
68+
}
69+
70+
if (e instanceof RuntimeException re) {
71+
throw re;
72+
}
73+
throw new RuntimeException(e);
4474
}
4575
}
4676

@@ -55,16 +85,39 @@ public boolean afterRun(TransformContext context) {
5585
// so we don't log the ClassTarget as that will cause duplication
5686
if (target instanceof Target.ClassTarget && target.className().contains("$")) return;
5787
logger.error("Access transformer %s, targeting %s did not apply as its target doesn't exist", transformation, target);
88+
89+
var problem = Problem.builder(MISSING_TARGET)
90+
.severity(ProblemSeverity.ERROR)
91+
.contextualLabel("The target " + target + " does not exist.")
92+
.build();
93+
reportProblem(problemReporter, transformation, problem);
5894
});
5995
errored = true;
6096
}
6197

6298
return !(errored && validation == AccessTransformerValidation.ERROR);
6399
}
64100

101+
static void reportProblem(ProblemReporter problemReporter, Transformation transformation, Problem problem) {
102+
// Report a problem for each origin of the transform
103+
for (String origin : transformation.origins()) {
104+
var m = ORIGIN_PATTERN.matcher(origin);
105+
ProblemLocation problemLocation;
106+
if (!m.matches()) {
107+
problemLocation = ProblemLocation.ofFile(Paths.get(origin));
108+
} else {
109+
var file = Path.of(m.group(1));
110+
var line = Integer.parseUnsignedInt(m.group(2));
111+
problemLocation = ProblemLocation.ofLocationInFile(file, line);
112+
}
113+
114+
problemReporter.report(Problem.builder(problem).location(problemLocation).build());
115+
}
116+
}
117+
65118
@Override
66119
public void visitFile(PsiFile psiFile, Replacements replacements) {
67-
var visitor = new ApplyATsVisitor(ats, replacements, pendingATs, logger);
120+
var visitor = new ApplyATsVisitor(ats, replacements, pendingATs, logger, problemReporter);
68121
visitor.visitFile(psiFile);
69122
if (visitor.errored) {
70123
errored = true;

accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@
1414
import com.intellij.psi.PsiRecursiveElementVisitor;
1515
import com.intellij.psi.PsiWhiteSpace;
1616
import com.intellij.psi.util.ClassUtil;
17-
import com.intellij.psi.util.PsiClassUtil;
1817
import net.neoforged.accesstransformer.parser.AccessTransformerFiles;
1918
import net.neoforged.accesstransformer.parser.Target;
2019
import net.neoforged.accesstransformer.parser.Transformation;
2120
import net.neoforged.jst.api.Logger;
2221
import net.neoforged.jst.api.PsiHelper;
2322
import net.neoforged.jst.api.Replacements;
23+
import net.neoforged.problems.Problem;
24+
import net.neoforged.problems.ProblemReporter;
2425
import org.jetbrains.annotations.NotNull;
2526
import org.jetbrains.annotations.Nullable;
2627

28+
import java.util.ArrayList;
2729
import java.util.Arrays;
30+
import java.util.Collections;
2831
import java.util.EnumMap;
2932
import java.util.Locale;
3033
import java.util.Map;
@@ -47,13 +50,15 @@ class ApplyATsVisitor extends PsiRecursiveElementVisitor {
4750
private final Replacements replacements;
4851
private final Map<Target, Transformation> pendingATs;
4952
private final Logger logger;
53+
private final ProblemReporter problemReporter;
5054
boolean errored = false;
5155

52-
public ApplyATsVisitor(AccessTransformerFiles ats, Replacements replacements, Map<Target, Transformation> pendingATs, Logger logger) {
56+
public ApplyATsVisitor(AccessTransformerFiles ats, Replacements replacements, Map<Target, Transformation> pendingATs, Logger logger, ProblemReporter problemReporter) {
5357
this.ats = ats;
5458
this.replacements = replacements;
5559
this.logger = logger;
5660
this.pendingATs = pendingATs;
61+
this.problemReporter = problemReporter;
5762
}
5863

5964
@Override
@@ -119,7 +124,7 @@ public void visitElement(@NotNull PsiElement element) {
119124
private void apply(@Nullable Transformation at, PsiModifierListOwner owner, PsiClass containingClass) {
120125
if (at == null) return;
121126
if (!at.isValid()) {
122-
error("Found invalid access transformer: %s. Final state: conflicting", at);
127+
error(at, "Found invalid access transformer. Final state: conflicting");
123128
return;
124129
}
125130

@@ -147,7 +152,7 @@ public String toString() {
147152
// If we're modifying a non-static interface method we can only make it public, meaning it must be defined as default
148153
if (containingClass.isInterface() && owner instanceof PsiMethod && !modifiers.hasModifierProperty(PsiModifier.STATIC)) {
149154
if (targetAcc != Transformation.Modifier.PUBLIC) {
150-
error("Access transformer %s targeting %s attempted to make a non-static interface method %s. They can only be made public.", at, targetInfo, targetAcc);
155+
error(at, "Access transformer targeting %s attempted to make a non-static interface method %s. They can only be made public.", targetInfo, targetAcc);
151156
} else {
152157
for (var kw : modifiers.getChildren()) {
153158
if (kw instanceof PsiKeyword && kw.getText().equals(PsiKeyword.PRIVATE)) { // Strip private, replace it with default
@@ -158,7 +163,7 @@ public String toString() {
158163
}
159164
} else if (containingClass.isEnum() && owner instanceof PsiMethod mtd && mtd.isConstructor() && at.modifier().ordinal() < Transformation.Modifier.DEFAULT.ordinal()) {
160165
// Enum constructors can at best be package-private, any other attempt must be prevented
161-
error("Access transformer %s targeting %s attempted to make an enum constructor %s", at, targetInfo, at.modifier());
166+
error(at, "Access transformer targeting %s attempted to make an enum constructor %s", targetInfo, at.modifier());
162167
} else if (targetAcc.ordinal() < detectModifier(modifiers, null).ordinal()) { // PUBLIC (0) < PROTECTED (1) < DEFAULT (2) < PRIVATE (3)
163168
modify(targetAcc, modifiers, Arrays.stream(modifiers.getChildren())
164169
.filter(el -> el instanceof PsiKeyword)
@@ -176,7 +181,7 @@ public String toString() {
176181
.findFirst()
177182
.ifPresent(replacements::remove);
178183
} else if (finalState == Transformation.FinalState.MAKEFINAL && !modifiers.hasModifierProperty(PsiModifier.FINAL)) {
179-
error("Access transformer %s attempted to make %s final. Was non-final", at, targetInfo);
184+
error(at, "Access transformer attempted to make %s final. Was non-final", targetInfo);
180185
}
181186
}
182187

@@ -250,11 +255,11 @@ private void checkImplicitConstructor(PsiClass psiClass, String className, @Null
250255

251256
var implicitAT = pendingATs.remove(new Target.MethodTarget(className, "<init>", desc));
252257
if (implicitAT != null && implicitAT.modifier() != detectModifier(psiClass.getModifierList(), classAt)) {
253-
error("Access transformer %s targeting the implicit constructor of %s is not valid, as a record's constructor must have the same access level as the record class. Please AT the record too: \"%s\"", implicitAT, className,
258+
error(implicitAT, "Access transformer targeting the implicit constructor of %s is not valid, as a record's constructor must have the same access level as the record class. Please AT the record too: \"%s\"", className,
254259
implicitAT.modifier().toString().toLowerCase(Locale.ROOT) + " " + className);
255260
pendingATs.remove(new Target.MethodTarget(className, "<init>", desc));
256261
} else if (classAt != null && detectModifier(psiClass.getModifierList(), null).ordinal() > classAt.modifier().ordinal() && implicitAT == null) {
257-
error("Access transformer %s targeting record class %s attempts to widen its access without widening the constructor's access. You must AT the constructor too: \"%s\"", classAt, className,
262+
error(classAt, "Access transformer targeting record class %s attempts to widen its access without widening the constructor's access. You must AT the constructor too: \"%s\"", className,
258263
classAt.modifier().toString().toLowerCase(Locale.ROOT) + " " + className + " <init>" + desc);
259264
pendingATs.remove(new Target.MethodTarget(className, "<init>", desc));
260265
}
@@ -294,8 +299,16 @@ private void injectConstructor(PsiClass psiClass, String className, Transformati
294299
" ".repeat(indent) + modifierString + psiClass.getName() + "() {}");
295300
}
296301

297-
private void error(String message, Object... args) {
298-
logger.error(message, args);
302+
private void error(Transformation transformation, String message, Object... args) {
303+
var problem = Problem.builder(AccessTransformersTransformer.INVALID_AT)
304+
.contextualLabel(String.format(Locale.ROOT, message, args))
305+
.build();
306+
AccessTransformersTransformer.reportProblem(problemReporter, transformation, problem);
307+
308+
var formatArgs = new ArrayList<>();
309+
Collections.addAll(formatArgs, args);
310+
formatArgs.add(transformation);
311+
logger.error(message + " at %s", formatArgs.toArray());
299312
errored = true;
300313
}
301314

api/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ java {
1919
dependencies {
2020
api "com.jetbrains.intellij.java:java-psi-impl:$intellij_version"
2121
api "info.picocli:picocli:$picocli_version"
22+
api "net.neoforged.installertools:problems-api:$problems_api_version"
2223
compileOnly "org.jetbrains:annotations:$jetbrains_annotations_version"
2324
}
2425

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
package net.neoforged.jst.api;
22

3-
public record TransformContext(IntelliJEnvironment environment, FileSource source, FileSink sink, Logger logger) {
3+
import net.neoforged.problems.ProblemReporter;
4+
5+
public record TransformContext(IntelliJEnvironment environment, FileSource source, FileSink sink, Logger logger, ProblemReporter problemReporter) {
6+
public TransformContext(IntelliJEnvironment environment, FileSource source, FileSink sink, Logger logger) {
7+
this(environment, source, sink, logger, ProblemReporter.NOOP);
8+
}
49
}

cli/src/main/java/net/neoforged/jst/cli/Main.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
import net.neoforged.jst.api.SourceTransformerPlugin;
66
import net.neoforged.jst.cli.io.FileSinks;
77
import net.neoforged.jst.cli.io.FileSources;
8+
import net.neoforged.problems.FileProblemReporter;
9+
import net.neoforged.problems.ProblemReporter;
10+
import org.jetbrains.annotations.Nullable;
811
import org.jetbrains.annotations.VisibleForTesting;
912
import picocli.CommandLine;
1013

1114
import java.nio.file.Path;
1215
import java.util.ArrayList;
1316
import java.util.HashSet;
1417
import java.util.List;
18+
import java.util.Objects;
1519
import java.util.ServiceLoader;
1620
import java.util.concurrent.Callable;
1721

@@ -41,9 +45,12 @@ public class Main implements Callable<Integer> {
4145
@CommandLine.Option(names = "--max-queue-depth", description = "When both input and output support ordering (archives), the transformer will try to maintain that order. To still process items in parallel, a queue is used. Larger queue depths lead to higher memory usage.")
4246
int maxQueueDepth = 100;
4347

44-
@CommandLine.Command(name = "--debug", description = "Print additional debugging information")
48+
@CommandLine.Option(names = "--debug", description = "Print additional debugging information")
4549
boolean debug = false;
4650

51+
@CommandLine.Option(names = "--problems-report", description = "Write problems to this report file.")
52+
Path problemsReport;
53+
4754
private final HashSet<SourceTransformer> enabledTransformers = new HashSet<>();
4855

4956
public static void main(String[] args) {
@@ -68,7 +75,8 @@ public static int innerMain(String... args) {
6875
public Integer call() throws Exception {
6976
var logger = debug ? new Logger(System.out, System.err) : new Logger(null, System.err);
7077
try (var source = FileSources.create(inputPath, inputFormat);
71-
var processor = new SourceFileProcessor(logger)) {
78+
var problemReporter = createProblemReporter(problemsReport);
79+
var processor = new SourceFileProcessor(logger, Objects.requireNonNullElse(problemReporter, ProblemReporter.NOOP))) {
7280

7381
if (librariesList != null) {
7482
processor.addLibrariesList(librariesList);
@@ -96,6 +104,15 @@ public Integer call() throws Exception {
96104
return 0;
97105
}
98106

107+
@Nullable
108+
private FileProblemReporter createProblemReporter(Path problemsReport) {
109+
if (problemsReport == null) {
110+
return null;
111+
} else {
112+
return new FileProblemReporter(problemsReport);
113+
}
114+
}
115+
99116
private void setupPluginCliOptions(List<SourceTransformerPlugin> plugins, CommandLine.Model.CommandSpec spec) {
100117
for (var plugin : plugins) {
101118
var transformer = plugin.createTransformer();

cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import net.neoforged.jst.api.TransformContext;
1414
import net.neoforged.jst.cli.intellij.ClasspathSetup;
1515
import net.neoforged.jst.cli.intellij.IntelliJEnvironmentImpl;
16+
import net.neoforged.problems.ProblemReporter;
1617

1718
import java.io.IOException;
1819
import java.io.UncheckedIOException;
@@ -33,11 +34,13 @@ class SourceFileProcessor implements AutoCloseable {
3334
private final IntelliJEnvironmentImpl ijEnv;
3435
private int maxQueueDepth = 50;
3536
private final Logger logger;
37+
private final ProblemReporter problemReporter;
3638

3739
private final List<String> ignoredPrefixes = new ArrayList<>();
3840

39-
public SourceFileProcessor(Logger logger) throws IOException {
41+
public SourceFileProcessor(Logger logger, ProblemReporter problemReporter) throws IOException {
4042
this.logger = logger;
43+
this.problemReporter = problemReporter;
4144
ijEnv = new IntelliJEnvironmentImpl(logger);
4245
ijEnv.addCurrentJdkToClassPath();
4346
}
@@ -47,7 +50,7 @@ public boolean process(FileSource source, FileSink sink, List<SourceTransformer>
4750
throw new IllegalStateException("Cannot have an input with possibly more than one file when the output is a single file.");
4851
}
4952

50-
var context = new TransformContext(ijEnv, source, sink, logger);
53+
var context = new TransformContext(ijEnv, source, sink, logger, problemReporter);
5154

5255
var sourceRoot = source.createSourceRoot(VirtualFileManager.getInstance());
5356
ijEnv.addSourceRoot(sourceRoot);

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ picocli_version=4.7.6
55
junit_version=5.10.3
66
assertj_version=3.26.0
77
gson_version=2.10.1
8+
problems_api_version=3.0.3

settings.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.gradle.api.initialization.resolve.RepositoriesMode
2+
13
pluginManagement {
24
repositories {
35
gradlePluginPortal()
@@ -13,6 +15,7 @@ plugins {
1315
}
1416

1517
dependencyResolutionManagement {
18+
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
1619
repositories {
1720
mavenCentral()
1821
maven {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

0 commit comments

Comments
 (0)