Skip to content

Commit 1fb9fce

Browse files
feat(plugin): add failOnUnresolved validation mode
1 parent 1be8e59 commit 1fb9fce

4 files changed

Lines changed: 197 additions & 5 deletions

File tree

bpj-maven-plugin/src/main/java/io/github/bpj/maven/BpjPrepareMojo.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public class BpjPrepareMojo extends AbstractMojo {
3737
@Parameter(defaultValue = "true")
3838
private boolean failOnError;
3939

40+
@Parameter(defaultValue = "false")
41+
private boolean failOnUnresolved;
42+
4043
@Parameter(defaultValue = "false")
4144
private boolean verbose;
4245

@@ -74,7 +77,7 @@ public void execute() throws MojoExecutionException {
7477
Files.createDirectories(target.getParent());
7578

7679
String original = Files.readString(source, StandardCharsets.UTF_8);
77-
TransformationResult result = transformer.transform(source, original);
80+
TransformationResult result = transformer.transform(source, original, failOnUnresolved);
7881
changedCalls += result.replacements();
7982

8083
Files.writeString(target, result.source(), StandardCharsets.UTF_8);

bpj-maven-plugin/src/main/java/io/github/bpj/maven/transform/BpjSourceTransformer.java

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
import com.sun.source.tree.IdentifierTree;
55
import com.sun.source.tree.ImportTree;
66
import com.sun.source.tree.LiteralTree;
7+
import com.sun.source.tree.LambdaExpressionTree;
78
import com.sun.source.tree.MemberSelectTree;
9+
import com.sun.source.tree.MethodTree;
810
import com.sun.source.tree.MethodInvocationTree;
911
import com.sun.source.tree.Tree;
12+
import com.sun.source.tree.VariableTree;
1013
import com.sun.source.util.JavacTask;
1114
import com.sun.source.util.SourcePositions;
15+
import com.sun.source.util.TreePath;
1216
import com.sun.source.util.TreePathScanner;
17+
import com.sun.source.util.TreeScanner;
1318
import com.sun.source.util.Trees;
1419
import java.io.IOException;
1520
import java.nio.charset.StandardCharsets;
@@ -62,11 +67,28 @@ public final class BpjSourceTransformer {
6267
* @return transformation result
6368
*/
6469
public TransformationResult transform(Path sourcePath, String source) {
70+
return transform(sourcePath, source, false);
71+
}
72+
73+
/**
74+
* Transforms one Java source file content.
75+
*
76+
* @param sourcePath source path used in parser diagnostics
77+
* @param source source code content
78+
* @param failOnUnresolved whether unresolved placeholder roots should fail transformation
79+
* @return transformation result
80+
*/
81+
public TransformationResult transform(Path sourcePath, String source, boolean failOnUnresolved) {
6582
Objects.requireNonNull(sourcePath, "sourcePath cannot be null");
6683
Objects.requireNonNull(source, "source cannot be null");
6784

6885
ParseContext parse = parse(sourcePath, source);
69-
List<Insertion> insertions = collectInsertions(sourcePath, parse.compilationUnit, parse.sourcePositions);
86+
List<Insertion> insertions = collectInsertions(
87+
sourcePath,
88+
parse.compilationUnit,
89+
parse.sourcePositions,
90+
failOnUnresolved
91+
);
7092
if (insertions.isEmpty()) {
7193
return new TransformationResult(source, 0);
7294
}
@@ -123,14 +145,23 @@ private void failIfParseErrors(Path sourcePath, DiagnosticCollector<JavaFileObje
123145
private List<Insertion> collectInsertions(
124146
Path sourcePath,
125147
CompilationUnitTree compilationUnit,
126-
SourcePositions sourcePositions
148+
SourcePositions sourcePositions,
149+
boolean failOnUnresolved
127150
) {
128151
List<Insertion> insertions = new ArrayList<>();
129152

130153
new TreePathScanner<Void, Void>() {
131154
@Override
132155
public Void visitMethodInvocation(MethodInvocationTree invocation, Void unused) {
133-
maybeCollect(sourcePath, invocation, compilationUnit, sourcePositions, insertions);
156+
maybeCollect(
157+
sourcePath,
158+
getCurrentPath(),
159+
invocation,
160+
compilationUnit,
161+
sourcePositions,
162+
insertions,
163+
failOnUnresolved
164+
);
134165
return super.visitMethodInvocation(invocation, unused);
135166
}
136167
}.scan(compilationUnit, null);
@@ -140,10 +171,12 @@ public Void visitMethodInvocation(MethodInvocationTree invocation, Void unused)
140171

141172
private void maybeCollect(
142173
Path sourcePath,
174+
TreePath invocationPath,
143175
MethodInvocationTree invocation,
144176
CompilationUnitTree compilationUnit,
145177
SourcePositions sourcePositions,
146-
List<Insertion> insertions
178+
List<Insertion> insertions,
179+
boolean failOnUnresolved
147180
) {
148181
if (invocation.getArguments().size() != 1) {
149182
return;
@@ -173,6 +206,19 @@ private void maybeCollect(
173206
return;
174207
}
175208

209+
if (failOnUnresolved) {
210+
List<String> unresolved = findUnresolvedRoots(roots, invocationPath);
211+
if (!unresolved.isEmpty()) {
212+
throw unresolvedPlaceholderException(
213+
sourcePath,
214+
compilationUnit,
215+
sourcePositions,
216+
invocation,
217+
unresolved
218+
);
219+
}
220+
}
221+
176222
long end = sourcePositions.getEndPosition(compilationUnit, argument);
177223
if (end < 0) {
178224
return;
@@ -257,6 +303,77 @@ private IllegalArgumentException invalidPlaceholderException(
257303
return new IllegalArgumentException(message);
258304
}
259305

306+
private List<String> findUnresolvedRoots(LinkedHashSet<String> roots, TreePath invocationPath) {
307+
Set<String> availableRoots = collectAvailableRoots(invocationPath);
308+
return roots.stream()
309+
.filter(root -> !availableRoots.contains(root))
310+
.toList();
311+
}
312+
313+
private Set<String> collectAvailableRoots(TreePath invocationPath) {
314+
LinkedHashSet<String> names = new LinkedHashSet<>();
315+
names.add("this");
316+
names.add("super");
317+
318+
for (TreePath current = invocationPath; current != null; current = current.getParentPath()) {
319+
Tree leaf = current.getLeaf();
320+
if (leaf instanceof MethodTree method) {
321+
for (VariableTree parameter : method.getParameters()) {
322+
names.add(parameter.getName().toString());
323+
}
324+
if (method.getBody() != null) {
325+
collectVariableNames(method.getBody(), names);
326+
}
327+
} else if (leaf instanceof LambdaExpressionTree lambda) {
328+
for (VariableTree parameter : lambda.getParameters()) {
329+
names.add(parameter.getName().toString());
330+
}
331+
collectVariableNames(lambda.getBody(), names);
332+
} else if (leaf instanceof com.sun.source.tree.ClassTree classTree) {
333+
for (Tree member : classTree.getMembers()) {
334+
if (member instanceof VariableTree field) {
335+
names.add(field.getName().toString());
336+
}
337+
}
338+
}
339+
}
340+
341+
return names;
342+
}
343+
344+
private void collectVariableNames(Tree tree, Set<String> names) {
345+
new TreeScanner<Void, Set<String>>() {
346+
@Override
347+
public Void visitClass(com.sun.source.tree.ClassTree node, Set<String> collector) {
348+
return null;
349+
}
350+
351+
@Override
352+
public Void visitVariable(VariableTree variable, Set<String> collector) {
353+
collector.add(variable.getName().toString());
354+
return super.visitVariable(variable, collector);
355+
}
356+
}.scan(tree, names);
357+
}
358+
359+
private IllegalArgumentException unresolvedPlaceholderException(
360+
Path sourcePath,
361+
CompilationUnitTree compilationUnit,
362+
SourcePositions sourcePositions,
363+
MethodInvocationTree invocation,
364+
List<String> unresolved
365+
) {
366+
long start = sourcePositions.getStartPosition(compilationUnit, invocation);
367+
long line = start >= 0 ? compilationUnit.getLineMap().getLineNumber(start) : -1;
368+
String location = line > 0 ? sourcePath + ":" + line : sourcePath.toString();
369+
370+
String message = "Unresolved BPJ placeholder root(s) " + unresolved
371+
+ " at " + location
372+
+ ". Define these variables in scope or disable this validation with "
373+
+ "<failOnUnresolved>false</failOnUnresolved>.";
374+
return new IllegalArgumentException(message);
375+
}
376+
260377
private String escapeBraces(String template) {
261378
return template
262379
.replace("{{", ESCAPED_OPEN_TOKEN)

bpj-maven-plugin/src/test/java/io/github/bpj/maven/transform/BpjSourceTransformerTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,66 @@ void run(String user) {
138138
assertTrue(exception.getMessage().contains("Demo.java:4"));
139139
}
140140

141+
@Test
142+
void shouldFailOnUnresolvedRootsWhenEnabled() {
143+
String source = """
144+
import io.github.bpj.BPJ;
145+
class Demo {
146+
void run(String name) {
147+
BPJ.println("Hello {missing}");
148+
}
149+
}
150+
""";
151+
152+
IllegalArgumentException exception = assertThrows(
153+
IllegalArgumentException.class,
154+
() -> transform(source, true)
155+
);
156+
157+
assertTrue(exception.getMessage().contains("Unresolved BPJ placeholder root(s) [missing]"));
158+
assertTrue(exception.getMessage().contains("Demo.java:4"));
159+
}
160+
161+
@Test
162+
void shouldNotFailOnUnresolvedRootsWhenValidationIsDisabled() {
163+
String source = """
164+
import io.github.bpj.BPJ;
165+
class Demo {
166+
void run(String name) {
167+
BPJ.println("Hello {missing}");
168+
}
169+
}
170+
""";
171+
172+
BpjSourceTransformer.TransformationResult result = transform(source, false);
173+
174+
assertEquals(1, result.replacements());
175+
assertTrue(result.source().contains("java.util.Map.of(\"missing\", missing)"));
176+
}
177+
178+
@Test
179+
void shouldResolveFieldNamesWhenFailOnUnresolvedIsEnabled() {
180+
String source = """
181+
import io.github.bpj.BPJ;
182+
class Demo {
183+
private final String name = "Ana";
184+
void run() {
185+
BPJ.println("Hello {name}");
186+
}
187+
}
188+
""";
189+
190+
BpjSourceTransformer.TransformationResult result = transform(source, true);
191+
192+
assertEquals(1, result.replacements());
193+
assertTrue(result.source().contains("java.util.Map.of(\"name\", name)"));
194+
}
195+
141196
private BpjSourceTransformer.TransformationResult transform(String source) {
142197
return transformer.transform(Path.of("Demo.java"), source);
143198
}
199+
200+
private BpjSourceTransformer.TransformationResult transform(String source, boolean failOnUnresolved) {
201+
return transformer.transform(Path.of("Demo.java"), source, failOnUnresolved);
202+
}
144203
}

docs/MAVEN_PLUGIN.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ Default: `true`
9696
If `true`, transformation errors fail the build.
9797
If `false`, plugin logs a warning and build continues.
9898

99+
### `failOnUnresolved`
100+
101+
Default: `false`
102+
103+
When `true`, BPJ fails the build if a placeholder root cannot be resolved from the current source scope.
104+
105+
Example:
106+
- Template: `"Hello {name} {missing}"`
107+
- In-scope variable: `name`
108+
- Missing variable: `missing`
109+
- Result with `failOnUnresolved=true`: plugin throws a clear error before compilation.
110+
99111
### `verbose`
100112

101113
Default: `false`
@@ -120,6 +132,7 @@ Logs transformed files and replacement counts.
120132
</executions>
121133
<configuration>
122134
<verbose>true</verbose>
135+
<failOnUnresolved>true</failOnUnresolved>
123136
</configuration>
124137
</plugin>
125138
</plugins>

0 commit comments

Comments
 (0)